21 KiB
CODE-REVIEW-657-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/657
- Материал:
git diff origin/dev...HEADна339379f504f7ab5c9142bbca887c363fc0645f1e(веткаissue/657-generated-at-merge, HEAD detached), 35 файлов, +1021/−136. Якоря совпадают:head=339379f5,tree=b63cbf25…,base=6d3738ad(origin/dev). - Класс: B (
scripts/**,.github/workflows/**,test/**) + C (PROCESS.md,AGENTS.md,docs/**) — класса A нет. - Этап: код-ревью (PROCESS.md §2.7), заход r1, блокирующих циклов израсходовано 0/4.
- Вердикт: красный.
Скоуп
Задача закрывает инфраструктурную часть SCOPE (конвейер ревью — не пользовательский
джоб, а обслуживание J1–J7 опосредованно, через скорость доставки; сама задача это
и объявляет как «инфраструктурная, вход сразу на S7»). Решения владельца: 1б —
docs/reviews/INDEX.md пересобирается только коммитами, идущими прямо в dev
(слияние кандидата, публикация SPEC-REVIEW), а не в ветке задачи; 2б — бандл
(dist/**, custom_components/houseplan/frontend/**) в дереве dev меняет только
релизный кандидат/бета (трейлер Release:), стенд dev.houseplan.tech берёт бандл
головы dev из артефакта Validate через служебную ветку dev-build.
AC из хендоффа — семь строк «AC · чем доказан · чем краснеет» (см. комментарий автора от 2026-09-26T05:59:00Z). Разбор велся по каждой строке отдельно чтением кода и, где это было практично, реальным исполнением (не моками) сценариев, которые юнит-тесты задачи проверяют только частично.
Как проверялось
Дешёвые гейты подтверждены зелёным Validate на этом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/36222411365, push) —
typecheck, npm test, npm run build + bundle-policy --verify, мутанты
(сharded) все прошли. Автор дополнительно прогнал полный node --test test/*.test.mjs
локально (шесть частей) и вручную все 11 новых мутантов (assertion-killed).
Перегонять эти гейты не стал — правило #343/условие честности сужения.
Дополнительно прогнал сам (не входит в стандартный набор для B/C-диффа, но необходимо для проверки защитного AC «индекс свеж после integrate»):
| Гейт | Команда | Результат |
|---|---|---|
| smoke-select | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
«Исполняемого frontend-диффа нет» — смоки не выбираются, диф не трогает src/** |
| воспроизведение сценария fast-forward (см. находку H1) | реальный git-репозиторий, оригинальный scripts/reviews-index.mjs без правок, без моков |
индекс красится — см. ниже |
Не прогонял: golden (npm run golden:verify), pytest tests_backend,
performance — диф не трогает demo/** (визуал), custom_components/**/*.py
или профили; AC их не называет. check-docs.mjs не обязателен: диф не
трогает src/** (§8, REVIEWER.md «Объём гейтов»).
Находки
H1 — после fast-forward слияния (dev не двигался за время ревью) docs/reviews/INDEX.md остаётся неполным, и Validate на dev красится на первом же таком merge
Заявленный AC (таблица хендоффа): «Индекс не пересобирается в ветке задачи;
после integrate свеж» — доказательство: test/reviews-index.test.mjs
«#635/#657 (1б)», rebase-generated (порядок шагов); краснеет — мутант
process-index-on-task-branch.
Что реально происходит. До этой задачи шаг «Опубликовать документ ревью»
(_process.yml) пересобирал docs/reviews/INDEX.md тем же коммитом, что и
сам документ, независимо от цели (if [ -f "$doc" ]; then … fi) — то есть каждый
код-ревью-коммит в ветке задачи уже нёс свежий индекс. Этот диф сузил условие до
[ "$target" = "dev" ] (истинно только для SPEC-REVIEW, публикуемого прямо в
dev; для CODE-REVIEW цель — ветка задачи, и индекс там больше не трогается —
это и есть решение 1б). Расчёт был на то, что merge-candidate.mjs дособерёт
индекс при слиянии («слиянием кандидата (merge-candidate.mjs, уже умеет)» — из
решения владельца). Но merge-candidate.mjs в этой задаче не менялся (нет в
диффе), и пересборка индекса (--commit-if-stale) в нём вызывается только
внутри rebaseOnto() — то есть только когда devMoved === true и требуется
ребейз. Когда dev не сдвинулся за время ревью (decideMerge({devMoved: false})
→ fast-forward, ветка merge-candidate.mjs:250-253), происходит прямой
pushWithLease(tip, 'dev', devNow) — индекс не пересобирается вообще. Это не
экзотика: это отдельная, специально тестируемая ветка решения
(test/merge-candidate.test.mjs: «dev не двигался: push кандидата как есть...»),
которая наступает всегда, когда во время ревью текущей задачи ни одна другая
задача не слилась в dev (тихие периоды, ночь, единственная задача в очереди —
именно то, чего #657 и добивается, снижая частоту параллельных слияний).
Воспроизведение (настоящий git, неизменённый scripts/reviews-index.mjs из
этой ветки — без моков, без правок):
git init -q -b dev
# … docs/reviews/CODE-REVIEW-1-r1.md + `reviews-index.mjs` → коммит, индекс свеж
node scripts/reviews-index.mjs --dir=docs/reviews --check # OK, «индекс свеж»
git checkout -q -b issue/700-x
# добавлен docs/reviews/CODE-REVIEW-700-r1.md, INDEX.md НЕ тронут (= поведение
# публикации документа в ветку задачи после этого диффа)
git commit -q -m "docs: review document for #700"
git checkout -q dev
git merge --ff-only -q issue/700-x # = decideMerge({devMoved:false}) → fast-forward
node scripts/reviews-index.mjs --dir=docs/reviews --check
# → "docs/reviews/INDEX.md устарел — пересобрать: node scripts/reviews-index.mjs"
# exit code: 1
Это ровно тот же гейт, что стоит в validate.yml на push в dev
(id: reviews_index, шаг «Индекс ревью совпадает с каталогом docs/reviews»,
validate.yml:104-107), и его провал не «continue-on-error» на уровне
итогового вердикта: шаг «Вердикт предполётных проверок» (validate.yml:211-234)
явно делает check "индекс ревью совпадает с каталогом" "$REVIEWS_INDEX" и
exit $fail — красный шаг красит весь preflight job, а значит и весь прогон
Validate на голове dev.
Почему это находка именно этой задачи, а не старый долг. До #657 индекс уже
приезжал в dev свежим при fast-forward, потому что ветка сама несла
самодостаточный INDEX-коммит (см. выше). #657 убрал этот путь для CODE-REVIEW
(единственный тип документа, идущий через merge-candidate.mjs), не добавив
взамен пересборку в фаст-форвардной ветке слияния. AC таблицы хендоффа
проверяет ровно две вещи — что публикация в ветку задачи индекс НЕ трогает
(верно) и что merge-candidate.mjs «всё ещё» вызывает --commit-if-stale» (тоже формально верно — вызывает, но не на этом пути) — и не проверяет само условие «после integrateсвеж» для фаст-форвардного слияния, потому чтоtest/merge-candidate.test.mjsработает на fakeOps и не в состоянии увидеть файловые последствия дляdocs/reviews/`.
Чем красится: сценарий выше — реальным git merge --ff-only +
reviews-index.mjs --check на неизменённом коде репозитория; это не
гипотетическая, а прямо воспроизводимая последовательность действий
конвейера при следующем же тихом слиянии.
Что нужно поправить (не мой выбор, но для ориентира владельцу): в
decideMerge/mergeCandidate фаст-forward ветка обязана либо тоже дергать
reviews-index --commit-if-stale --issue=$issue на tip перед
pushWithLease, либо публикация документа в ветке задачи обязана сама
включать свежий INDEX-коммит для случая, когда слияние окажется
fast-forward (тогда придётся вернуть часть анализа конфликтов, которого 1б
избегает). Выбор архитектуры — авторский; здесь фиксируется только то, что
текущий код инвариант не держит.
Серьёзность: High (блокирует). AC таблицы хендоффа для этой строки не
выполнен на реальном, регулярно достижимом пути; последствие — не тестовый
провал в ветке задачи, а красный Validate на dev после штатного слияния,
без явной причины в диффе того же дня (следующий разработчик будет
разбираться, что сломало dev, не находя в своём коммите ничего relevant).
Что проверено и корректно
- Бандл-политика (2б), правило коммита.
scripts/bundle-policy.mjs(bundleCommitErrors,isBundlePath,releaseTrailers) и его подключение вvalidate-commit-provenance.mjs(и хукcommit-msg, и история в CI) — прочитано полностью, логика согласована сtest/bundle-policy.test.mjs. Хук судит без даты автора (authorDateне передаётся) — значит, судится всегда, а разграничение поBUNDLE_RELEASE_ONLY_SINCEработает только для ретроспективной проверки истории в CI, что и заявлено («в хуке даты нет — судится всегда»). Проверено чтением и сверкой с тестом, тест умеет падать (мутантыbundle-policy-rule-off,bundle-policy-cutoff-inverted). --verify/--must-match(сверка копий только на кандидате).committedBundleMustMatchкорректно решает по путям коммита ИЛИ поisCandidateSubject(кандидат, забывший пересобрать бандл, всё равно сверяется) — прочитано, обосновано тестом (bundle-policy-always-compares,bundle-policy-candidate-not-compared). Отдельно проверил вручную:git diff-tree -r --no-commit-id --name-only HEADдля настоящего двухродительского merge-коммита возвращает пустой список (эксперимент в/tmp/mergetest) — то естьcommitShape()в этом (маловероятном при линейной моделиdev) случае решит «копии не сверяются» даже если бандл менялся. Это не считаю находкой уровня Medium:merge-candidate.mjsне создаёт двухродительских коммитов вdev(только fast-forward/rebase-push), аvalidateCommitMessage's собственный range-check уже сознательно пропускаетparentCount > 1тем же способом (continue) — новый код просто следует уже принятому в кодовой базе допущению. Фиксирую как наблюдение, не находку.assertCommittedBundleFresh/ публикация беты. Читает манифест из закоммиченного дерева публикуемого SHA (не с диска), сверяет сsourceFingerprint(root)при чистом дереве на этом SHA — корректно; мутантrelease-prerelease-skips-fresh-bundleдоказывает, что вызов не выпадает молча.bundle-sync --release/bundle:clean.TARGETSформируется по наличию--release; без флага HACS-копия не трогается — проверено чтением и тестомtest/bundle-sync.test.mjs(реальныйspawnSync, не мок).bundle-policy.mjs --clean(git checkout --,git clean -fdq) возвращает ровно корни бандла, не трогая остальное дерево — тест это подтверждает (notes.txtостаётся).dev-build.mjs/ стенд.publishDevBuildстроит коммит без родителя во временном индексе (не трогая рабочее дерево источника), публикует только еслиexpectRefне ушёл вперёд, иначе тихо уступает следующему прогону — прочитано и подтвержденоtest/dev-build.test.mjsна настоящих git-репозиториях (bare + клон), включая сценарий стенда (update-dev-bundle.sh --reset/ без флага).validate.yml:dev_buildjob условиеpush && ref == refs/heads/dev,continue-on-error: true, не входит вneedsjobproof— подтверждено и тестомtest/validate-workflow.test.mjs, и чтением самого workflow.rebase-on-dev.mjs. Конфликт по бандлу теперь берёт версиюdevбез пересборки/amend — согласовано с тем, что ветка бандл не несёт;GENERATED_ROOTSтеперь читается изbundle-policy.BUNDLE_ROOTS, единая точка. Тесты (test/rebase-on-dev.test.mjs) переписаны без фальшивой пересборки и это корректно отражает новое поведение.docs/reviews— переключение публикации наtarget=dev. Само условие в_process.yml(строкаif [ -f "$doc" ] && [ "$target" = "dev" ]; then) и соответствующий мутант (process-index-on-task-branch) корректны как локальное решение; ограничение находки H1 — не в этом условии, а в отсутствующей компенсации на сторонеmerge-candidate.mjs.- Второй коммит (
339379f5) — понижение базыbundleBytes. Причинно обосновано в теле коммита (Validate на первом коммите падал на храповике связности монолита из-за постороннего дрейфа размераdistпосле хотфиксов #649, не из-за этой задачи); не расширяет скоуп. - Трейлеры. Оба коммита несут
Issue: #657иUser-Visible: no— корректно: класса A нет, видимого пользователю поведения нет, changelog не требуется. - Одно число — один источник. Число job-загрузок артефакта
card-bundle(было 4, стало 5 — новаяdev_build) сверено вручную по самому workflow (grep -n "name: card-bundle"— ровно 5 вхождений: 1 upload + 4 download:dev_build,smoke, и два прочих браузерных job) и совпадает с тем, что проверяетtest/validate-workflow.test.mjs.
Чего не проверял
- Golden, pytest, performance-профили — диф не трогает визуал/Python/ профили, AC их не называет.
check-docs.mjs— не обязателен (диф не поsrc/**); заявление автора, что этот гейт независимо красный на скриншотах и на базеdev, не перепроверял (не относится к предмету ревью).- Живой прогон
merge-candidate.mjsпротив настоящего GitHub API — не запускал (нужны токен и реальный репозиторий); H1 доказан на уровне логики и на уровнеdocs/reviews/INDEX.md-инварианта напрямую, что для дефекта этого типа достаточно и без интеграции сgh. - Двухродительские merge-коммиты в
dev— отмечено выше как воспроизводимое, но не относящееся к используемому в проекте потоку (см. «Что проверено и корректно»); не поднимаю до находки.
Итог
AC "индекс ревью не пересобирается в ветке задачи; после integrate свеж"
не выполняется для fast-forward пути слияния — реального, тестируемого и,
по всей вероятности, частого исхода decideMerge(). Это ломает гейт
"индекс ревью совпадает с каталогом" на dev без видимой причины в
последующих диффах. Остальная часть задачи (бандл-политика 2б, стенд dev,
переход публикации документа на target=dev) прочитана и перепроверена
(частично исполнением) без находок.
Вердикт: красный · заход r1 · блокирующих циклов 0/4 · High: 1 · Medium: 0 → в задаче · Документ: docs/reviews/CODE-REVIEW-657-r1.md
Материал раунда
- Ветка:
issue/657-generated-at-merge, коммит339379f504f7— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
b63cbf255cc76f784688d9ad7553444f9a89e337git log --all --format='%H %T' | grep b63cbf255cc7 - Тело issue:
e15e73b22b32d0524d13035dc170c366dd9aa8a038a6ebb5dab193b2868f5043 - Вердикт конвейера:
red· High 1