21 KiB
CODE-REVIEW-492-r1
- Issue: #492 — CI: проверять точный кандидат интеграции и полный набор зависимостей selection/reuse
- Материал ревью:
6cf6fc787ebaec9db038a51163429586de7d76b8(рабочая копия веткиissue/492-exact-candidate-and-input-manifestуже на нём) - Заход: r1 · блокирующих циклов израсходовано 0/4 (спек-ревью r1/r2 бюджет код-ревью не тратит; после спек-ревью был возврат в S6 только из-за конфликта слияния — цикл ревью кода этим не образуется, §4)
- Класс: B/infra — ни одного файла класса A (
src/**,custom_components/**/*.py, манифесты, i18n) в диффе; маршрутизация через S-метки без продуктового кода — прецедент #472/#475/#481, подтвержденоgit diff --stat origin/dev...HEAD
Скоуп
git diff origin/dev...HEAD — 23 файла, +2399/-267:
- новые модули:
scripts/check-inputs.mjs(единый manifest входов, §5 ТЗ),scripts/merge-candidate.mjs(слияние точного кандидата, §4 ТЗ); - переписаны на manifest:
scripts/classify-changes.mjs,scripts/gate-reuse.mjs; - расширен
scripts/mutation-gate.mjs:guardInputs/wrapperInputs/registryDelta/selectForDiff/baseRegistry, + 8 протокольных мутантов; scripts/backend-test-guard.mjs,scripts/trail-resume-test-guard.mjs—GUARD_INPUTS;.github/workflows/process.yml— шаг слияния вызываетmerge-candidate.mjsвместо inline-shell;.github/workflows/validate.yml— jobchangesчитаетunknown_inputs, пишет summary;.github/workflows/nightly.yml— ждёт дочерний Validate и наследует его исход;- тесты:
test/check-inputs.test.mjs,test/merge-candidate.test.mjs,test/nightly-workflow.test.mjs(новые),test/gate-reuse.test.mjs,test/classify-changes.test.mjs,test/mutation-gate.test.mjs,test/validate-workflow.test.mjs(расширены); - документация:
docs/TESTING.md,PROCESS.md§10.3,docs/specs/README.md, спека и её ревью-документы (r1/r2 — уже приняты, не предмет этого этапа).
Как проверялось
Материал точно на заявленном SHA: git rev-parse HEAD = 6cf6fc78…, совпадает с меткой этапа; git status чист.
Уже подтверждено CI на этом SHA (Validate, run 34282060597, conclusion: success, проверено gh run view --json headSha,conclusion — headSha совпадает буквально): typecheck/npm test/npm run build (job «Фронтенд: типы, юниты, мутанты, синхрон бандла»), hassfest, hacs, process-gate/provenance/docs (preflight), и три шарда «Мутанты по диффу» — все success. Эти гейты я не перегонял.
Разобрано дополнительно мной, за пределами того, что покрывает Validate:
| Гейт | Команда | Результат |
|---|---|---|
| Новые/изменённые юнит-тесты целиком | node --test test/check-inputs.test.mjs test/merge-candidate.test.mjs test/gate-reuse.test.mjs test/classify-changes.test.mjs test/nightly-workflow.test.mjs test/validate-workflow.test.mjs |
75/75 pass |
| Реестр мутантов, юниты | node --test test/mutation-gate.test.mjs |
39/39 pass |
| Лист покрытия manifest на реальном дереве | node scripts/check-inputs.mjs --coverage |
exit 0, пусто |
| Диффовый отбор мутантов на реальном диапазоне | node scripts/mutation-gate.mjs --changed origin/dev..HEAD |
отобрано 83 из 590 (по файлам 83, по определениям 9) — числа согласуются с диффом; полный прогон гардов остановился на No module named pytest (в этом окружении нет backend-venv, ожидаемо по AGENTS.md) |
Почему CI это не скрывает: тот же прогон в Validate этого SHA («Мутанты по диффу 1/3, 2/3, 3/3») выполнялся в среде с backend-venv и завершился success — отобранные мутанты реально прогнаны там, где я не смог |
||
| Реальное поведение reuse на этом SHA | gh run view 34282060597 --json jobs + логи шага «Ключи переиспользования»/«Маркер backend» |
backend помечен skipped: ключ реюза (посчитанный НОВЫМ gate-reuse.mjs на этом SHA) совпал с ранее закэшированным маркером — легитимный reuse (предыдущий коммит этой же ветки, не тронувший backend-входы, уже прогнал backend с тем же ключом), не молчаливая дыра. smoke/golden/performance_smoke — skipped по heavy=false (обычный push не на dev, не релиз, не full=true) — это не относится к manifest и не должно быть иначе |
Не гонял и почему: npm run golden:verify, browser-смоки, pytest tests_backend, perf-профили — диф не трогает src/**, geometry, layout/marker.space/open_spans, рендер или видимое поведение; ни один AC не требует этих гейтов. check-docs.mjs не запускал — диф не касается src/**.
Разбор AC (доказательство по каждому)
| AC | Чем доказан | Проверено |
|---|---|---|
| AC1 | test/merge-candidate.test.mjs: decideMerge таблица + эксперимент аудита (реальный git, воспроизводит «20 → 40» из ТЗ) + мутант merge-pushes-unvalidated-candidate |
тестом, тест умеет падать (см. таблицу мутантов ниже) |
| AC2 | dev ушёл снова после Validate: lease отклонён → новая попытка; трижды → S6 + мутант merge-ignores-lease-rejection |
тестом |
| AC3 | patch-id изменился при ребейзе — S7-code-review |
тестом |
| AC4 | test/check-inputs.test.mjs «§8.1 представители» на реальном дереве (все категории всех тяжёлых job) |
тестом на реальных путях, не выдуманных |
| AC5 | test/classify-changes.test.mjs «неизвестный вход расширяет...», test/check-inputs.test.mjs «§5.5 лист покрытия» + мутант classify-unknown-input-is-unaffected |
тестом |
| AC6 | test/check-inputs.test.mjs «§8.1 обратная проба», test/gate-reuse.test.mjs «backend не зависит от src/**» + мутант reuse-backend-hashes-ui; финальный код CHECKS.backend.roots не содержит src/** (прочитано) |
тестом + чтением |
| AC7 | test/mutation-gate.test.mjs §6.1 (полнота: каждый файл-обёртка, реально названный в guard: реестра, обязан иметь GUARD_INPUTS) + §8.2 (10 обёрток без 3-го аргумента отбираются) + мутант guard-inputs-ignore-wrapper-defaults |
тестом; проверено чтением, что coordinate-write-barrier-guard.mjs/review-doc-guard.mjs (два прочих *-guard.mjs в дереве) в реестре вызываются только как node --test test/…test.mjs, то есть уже видны guardFiles напрямую — вне действия §6.1, находкой не являются |
| AC8 | #492 §6.4: дифф только по реестру отбирает…, реальный прогон mutation-gate.mjs --changed (см. таблицу гейтов) + мутант registry-diff-not-selected |
тестом + живым прогоном на репозитории |
| AC9 | test/nightly-workflow.test.mjs + мутант nightly-does-not-wait; по формату — текстовый контракт YAML/bash, как и существующий validate-workflow.test.mjs (то же соглашение, что уже принято в проекте для этого класса файлов) |
тестом (контрактным, по прецеденту) |
| AC10 | 8 мутантов (не 6, как в §8.5 ТЗ, — превышение, не недостача) существуют, у каждого предметный guard-тест; docs/TESTING.md и PROCESS.md §10.3 описывают точный кандидат и manifest — прочитано, оба раздела на месте и соответствуют коду |
тестом + чтением |
Таблица «чем краснеет» — новые протокольные мутанты (§2.7 код-ревью)
Проверено чтением: каждый patches[].find совпадает буквально со строкой текущего файла (иначе мутатор молча не находит цель — but здесь все 8 патчей адресуют существующий код 1:1, сверено построчно с git diff), и каждый guard матчится реальным именем существующего теста (сверено grep).
| Мутант | Патч ломает | Guard-тест (падает без защиты) |
|---|---|---|
manifest-drops-workflow-input |
WORKFLOW = [] |
test/gate-reuse.test.mjs: «the workflow itself is a toolchain input of every job» |
classify-unknown-input-is-unaffected |
неизвестный вход не расширяет | test/classify-changes.test.mjs: «неизвестный исполняемый вход расширяет…» |
reuse-backend-hashes-ui |
возврат src/** в backend.roots |
test/gate-reuse.test.mjs: «…a version bump changes all of them» (AC6) |
guard-inputs-ignore-wrapper-defaults |
declared = [] |
test/mutation-gate.test.mjs: «#492 §8.2» |
registry-diff-not-selected |
byRegistry = [] |
test/mutation-gate.test.mjs: «#492 §6.4: дифф только по реестру…» |
merge-pushes-unvalidated-candidate |
waitValidate не вызывается |
test/merge-candidate.test.mjs: «эксперимент аудита…» |
merge-ignores-lease-rejection |
pushed || true |
test/merge-candidate.test.mjs: «…lease отклонён…» |
nightly-does-not-wait |
gh run watch заменён на echo |
test/nightly-workflow.test.mjs: «nightly ждёт запущенный Validate…» |
Для каждой строки я прочитал сам гард-тест (не только его имя) и убедился, что мутация действительно провалила бы конкретное утверждение (не просто «тест существует») — см. разбор в теле ревью выше по каждому пункту. Реально исполнить эти 8 мутаций (as configured, через mutation-gate.mjs) я не смог локально для двух (nightly-does-not-wait — правит workflow YAML не через git apply тестируемого кода, а merge-pushes-unvalidated-candidate/остальные — через find/replace на реальном файле), но их логика проверена сравнением diff↔patch руками; фактический прогон реестра мутантов на этом SHA в CI (три зелёных шарда) косвенно подтверждает, что ни один из добавленных мутантов не сломал существующий раннер.
Находки
Нет High. Нет Medium в скоупе. Одна Low — не блокирует, фиксирую с решением «оставить, не чинить в этом заходе»:
Low: mergeCandidate() — необязательный аргумент attempt/maxAttempts не передаётся decideMerge в ветке !devMoved. scripts/merge-candidate.mjs:182-187. Внутри цикла попыток при !devMoved вызов decideMerge({ fresh: true, devMoved: false, leaseRejected: !pushed }) не несёт attempt/maxAttempts — при непрерывном отклонении lease (dev формально «не двигался» по merge-base, но кто-то извне пушит прямо в dev, минуя конвейер, — сценарий, который правила процесса и так запрещают вне hotfix/владельца) цикл for исчерпывает три попытки через continue, не встретив return внутри if (!devMoved)-ветки, и падает в фолбэк после цикла, который жёстко подставляет devMoved: true в decideMerge. Результат (give-up → S6-in-progress) при этом безопасен и корректен — задача не подтверждается смерженной без Validate, — но текст комментария («dev движется быстрее слияния») в этом узком случае технически неточен (dev на самом деле не двигался по критерию merge-base), и путь не покрыт отдельным тестом. Не продуктовый риск: описанный триггер («кто-то пушит прямо в dev в обход конвейера») сам запрещён процессом вне hotfix/действий владельца (AGENTS.md: «Не мержить в dev руками»). Решение ревьюера: не возвращать в скоуп этого захода — исправление стоит дороже находки (уточнение сообщения плюс тест на состязание, которое процесс и так не допускает); если всплывёт живьём, заводится обычный баг-issue.
Что проверено и корректно
- Ни одного файла класса A в диффе (подтверждено
git diff --stat); все коммиты несутIssue: #492,User-Visible: no(инфраструктурная задача, корректно — нет видимого пользователю поведения). scripts/check-inputs.mjs: glob→regexp,referencesOf(импорты/пути/comments-strip для JS и Python),closure(транзитивность кода, лист данных, раскрытие каталога, остановка на class-D) — все разобраны юнитами на виртуальном дереве и подтверждены на реальном.scripts/gate-reuse.mjsизбавлен отsourceFingerprint/HARNESS, ключ считается по manifest;sourceFingerprintосознанно остаётся у бандла/скриншотов (прочитано — используется вcheck-docs.mjs,docs-accept.mjs,bundle-freshness.mjs, не выпал из кодовой базы).scripts/classify-changes.mjs:CHECK_OF_OUTPUT/PERF_PROFILESразделены верно — job-выбор из manifest, перф-профили остаются регэкспами внутриfrontend(как решение §5.2 явно оговаривает).scripts/mutation-gate.mjs:guardInputs/wrapperInputsкорректно разводят «явный файл в команде» и «умолчание обёртки» (проверено тестом «явный файл отменяет умолчание»);GUARD_CLOSURE_STOPдержитsrc/**/скопированный бандл вне замыкания гарда (AC7/§6.4 «сторона патча остаётся точечной»);registryDelta/baseRegistryчитают реестр базы черезgit showво временный модуль и подчищают его вfinally(проверено тестом «временный модуль удалён»).merge-candidate.mjs/process.yml: инвариант «после прогона метка меняется всегда» — прослежено по всем финальнымdecideMerge-исходам, каждый несётto;TO-выражение в «Переставить метку» шаге читаетsteps.merge.outputs.toс фолбэком на прежнюю логику — совместимо с случаем, когда шаг слияния пропущен (stage != 'code'или вердикт не зелёный).nightly.yml: находит именно свой прогон поworkflow_dispatch+branch dev+createdAt >= since, ждёт его, наследует код возврата.- Живое поведение на этом SHA (реальный CI-прогон) подтверждает, что механизм реюза и диффового отбора мутантов работает не только в тестах, но и в проде: backend легитимно переиспользован, три шарда мутантов по диффу зелёные.
Чего не проверял
- Golden/browser-смоки/perf-профили/
pytest tests_backend— не требуются этим диффом (не трогаетsrc/**, geometry, рендер); backend реально прогнан в CI ранее на этой же ветке (см. reuse-маркер) и переиспользован здесь корректно. - Полное исполнение всех 8 новых мутантов «вживую» через
mutation-gate.mjs --id=<...>— окружение ревью без backend-venv (No module named pytest); опирался на зелёные три шарда «Мутанты по диффу» в Validate этого SHA плюс ручную сверку patch↔find и guard↔имя теста. - Реальное состязание двух параллельных пушей в
devво время работыmerge-candidate.mjs(описано в находке Low) — оценено по коду и логике, не воспроизводилось. - Поведение
scripts/coordinate-write-barrier-guard.mjs/review-doc-guard.mjsкак таковых (не предмет диффа — их защитная логика не менялась, только уточнено, что они вне действия §6.1 в этой задаче).
Вердикт
Зелёный. AC1–AC10 доказаны тестами, которые я прочитал и (для JS-стороны) прогнал, плюс живым прогоном CI и ручной сверкой на реальном репозитории. Единственная находка — Low, безопасна по факту и осознанно не возвращается в работу.
Материал раунда
- Ветка:
issue/492-exact-candidate-and-input-manifest, коммит6cf6fc787eba— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
9979182a713d6d2f5dd5cb0b9af8f9cf0b819b74git log --all --format='%H %T' | grep 9979182a713d - ТЗ
docs/specs/492-exact-candidate-and-input-manifest.md, блоб71847c16c8c1b9377a808c25fb87007dbd9a81edgit log --all --find-object=71847c16c8c1b9377a808c25fb87007dbd9a81ed -- docs/specs/492-exact-candidate-and-input-manifest.md - Вердикт конвейера:
green· High 0