18 KiB
CODE-REVIEW-475-r1
Issue: #475 · «mutation-gate --check не отличает живого свидетеля от мёртвого»
Трек: small (лёгкий) · Этап: код-ревью · Заход: r1 · блокирующих циклов 0/2
SHA материала: c0207af40f25564eac9773d583cdcc7f5c56ea45 (git rev-parse HEAD перед выводом)
Ветка приведена к dev конвейером до ревью: поверх легло 4 коммит(ов) dev
(9b41ac1c → c0207af4). Диапазон origin/dev..HEAD после этого содержит
ровно один коммит задачи — разбор полный (§7.2), а не по дельте, как и
предписано при ребейзе на ушедший вперёд dev.
Скоуп
Диапазон git diff origin/dev...HEAD, 4 файла, все класса B:
.github/workflows/validate.yml— новая jobchanged_mutants;scripts/mutation-gate.mjs—guardFiles(), расширенныйselectChangedMutants(), 2 новых мутанта;test/mutation-gate.test.mjs— AC1–AC3, AC5–AC7;test/validate-workflow.test.mjs— контрактный тест на YAML (AC4).
Класса A ни один файл не задет — трек small, ТЗ в теле issue, ревью ТЗ
прошло два захода (r1 жёлтый/Medium, r2 зелёный) и закрыто до кода.
Как проверялось
Гейты, соразмерно классу B без изменений в src/** (issue #127, PROCESS.md §8):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
зелёный, 7.2s |
| Юниты | npm test |
2083 pass / 0 fail / 1 skip, 38.7s (совпадает с числом автора в хендоффе) |
| Сборка + синхрон бандла | npm run build затем cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
зелёный, байт-в-байт совпадение |
Новый код не добавляет any |
node scripts/no-new-any.mjs --base origin/dev --head HEAD |
«Проверено добавленных строк в src/**/*.ts: 0» — диф не касается src/** |
| Отбор браузерных смоков | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
«Исполняемого frontend-диффа нет (src/**/*.ts не тронут)» — смоки не выбраны, выбирать нечего |
| Реестр мутантов применим | node scripts/mutation-gate.mjs --check |
509 мутантов, все ok, включая 2 новых |
Мутант changed-selection-ignores-guard-files |
node scripts/mutation-gate.mjs --id=changed-selection-ignores-guard-files |
«чистый прогон: ok», «тест покраснел, как обязан», поймано 1/1 |
Мутант changed-selection-matches-any-token |
node scripts/mutation-gate.mjs --id=changed-selection-matches-any-token |
то же, поймано 1/1 |
| Самоприменение отбора | node scripts/mutation-gate.mjs --changed=origin/dev..HEAD |
«файлов в диффе 4, мутантов затронуто 4 из 509» — независимо воспроизведено, состав описан ниже |
| AC7 напрямую | node -e "...selectChangedMutants(MUTANTS, [...])..." |
все 4 бэкенд-мутанта frontend_registration.py/test_ha_frontend_registration.py отобраны и по гарду, и по патчу |
Не прогонялось и почему:
check-docs.mjs— правило #127/§8 требует его при диффе поsrc/**; diff файловsrc/**не содержит. Пропуск честный, не молчаливый.python -m pytest tests_backend -q— окружение ревью не содержит.venv-backendиpytest(pip show pytest→ not found), это задокументированное ограничение (PROCESS.md «Backend» и AGENTS.md «Environments»), не результат этой задачи. При попытке независимо прогнать--changed=origin/dev..HEADдо конца скрипт сам упёрся в это же: мутантtyping-gate-stops-running(не из #475, патчит.github/workflows/validate.yml) требуетpytest, которого здесь нет. AC7 проверен напрямую через API функции (см. таблицу) вместо прогона pytest.golden:verify, model-invariants, performance-профили, single-source-numbers — diff не меняет визуал, геометрию, ссылки на неё или пользовательские числа; неприменимо.
Находки
Medium (в скоупе) — триггер job не покрывает третье условие контракта
Файл: .github/workflows/validate.yml:410
Что не так: Контракт §2 принятого ТЗ (тело issue #475, редакция после фикса Medium из SPEC-REVIEW r1) требует:
job запускается при
frontend == 'true'илиbackend == 'true'или измененииscripts/mutation-gate.mjs
Реализовано:
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
Третье условие отсутствует — ни в if:, ни в контрактном тесте
test/validate-workflow.test.mjs:290, который проверяет ровно эти же два
дизъюнкта и не более.
Воспроизведение (не гипотеза — проверено чтением классификатора):
changes job классифицирует файлы двумя regex (validate.yml:262-263):
frontend: ^(src/|demo/|test/|dist/|custom_components/houseplan/frontend/|package(-lock)?\.json$|rollup\.config\.mjs$|tsconfig)
backend: ^(custom_components/.*\.py$|tests_backend/|scripts/support-relay/|pytest\.ini$)
scripts/mutation-gate.mjs не начинается ни с одного из перечисленных
префиксов и не совпадает ни с одним $-якорем — ни frontend, ни backend
не станут true от диффа, ограниченного этим файлом. Значит правка
scripts/mutation-gate.mjs (например, новый мутант, патчащий
src//custom_components/-файл, без сопутствующей правки в test/**)
проходит без единого запуска changed_mutants — job попросту не
запланируется на этом пуше.
Это ровно тот класс дефекта, ради которого заведён #475: рефакторинг файла,
отвечающего за отбор мутантов, остаётся невидимым до недельного полного
прогона (или до релиза) — тот же разрыв «неделя–релиз» из #466/#467, только
теперь применительно к самому гейту, а не к продуктовому коду. Contract §2
называет это условие явно и с тем же обоснованием, каким объяснено условие
backend («бэкенд-мутанты патчат .py... дифф... даёт backend=true без
frontend=true») — то есть это не пропущенная деталь ТЗ, а нереализованная
часть уже принятого контракта.
Почему не High: основной сценарий (правка продуктового/тестового кода,
меняющая patch.file или файл гарда любого мутанта) работает и подтверждён
и юнитами, и самоприменением. Пробел узкий — правка ограничена буквально
одним файлом scripts/mutation-gate.mjs без сопутствующих test/**/src/**/
custom_components/**.py изменений — но контракт именно этот случай называет
по имени, поэтому строка контракта не выполнена.
Чем закрывается (в скоупе, без выхода за задачу): добавить в if:
третий дизъюнкт, обнаруживающий изменение scripts/mutation-gate.mjs
(например, отдельным шагом changes/classify либо прямой проверкой пути
диапазона в самой job), и покрыть его тем же контрактным тестом.
Low — тест AC7 не покрывает четвёртый бэкенд-мутант, хотя код это делает
Файл: test/mutation-gate.test.mjs:259–267
В реестре ровно 4 мутанта, патчащих custom_components/houseplan/frontend_registration.py
с гардом tests_backend/test_ha_frontend_registration.py (scripts/mutation-gate.mjs:120,132,146,158).
Тест AC7 фильтрует их через id.startsWith('frontend-registration-'), что
даёт только 3 — четвёртый называется frontend-reload-notice-forgets-persisted-flag
(другой префикс id, тот же патч-файл и гард) и в assert-цикл не попадает.
Проверено напрямую (не через тест): selectChangedMutants реально отбирает
все 4 и по гарду, и по патчу — сама реализация не различает мутанты по имени,
поэтому регрессии здесь нет, только тест не проверяет ровно то, что заявляет
ТЗ («отбирает все четыре frontend-registration-* мутанта»). Наименование в
ТЗ тоже неточно (не все четыре носят префикс frontend-registration-).
Снимаю без исправления: механизм общий, не завязан на имя мутанта, и
пропущенный четвёртый защищён тем же кодовым путём, что и проверенные три —
регрессия, ломающая только его и не ломающая остальные три, потребовала бы
отдельного условия по конкретному id, которого в реализации нет.
Проверка AC — таблица «чем доказан / чем краснеет»
| AC | Доказано | Чем | Чем краснеет |
|---|---|---|---|
| AC1 (патч-файл в диффе → отбор) | unit | test/mutation-gate.test.mjs:228 (#475 AC1) + ранее существовавший #332 тест |
защитный AC без мутанта не заявлен (простое сравнение выборки) |
| AC2 (файл гарда — тест/смок/pytest — в диффе → отбор) | unit + мутант | test/mutation-gate.test.mjs:233 (#475 AC2) |
мутант changed-selection-ignores-guard-files: гвард (--test-name-pattern="#475 AC2") чист на исходном коде и красный на патче (проверено: --id=changed-selection-ignores-guard-files → «поймано 1 из 1») |
| AC3 (токены-нефайлы не считаются) | unit + мутант | test/mutation-gate.test.mjs:241 (#475 AC3) |
мутант changed-selection-matches-any-token: проверено аналогично, «поймано 1 из 1» |
| AC4 (job в validate.yml: триггер, база диапазона, python-зависимости, блокирующая) | частично unit, частично чтением | test/validate-workflow.test.mjs:283 + чтение validate.yml:407-459 |
не выполнено полностью — триггер не покрывает третий дизъюнкт контракта (см. находку Medium); база диапазона, установка Python и отсутствие continue-on-error подтверждены и текстом job, и сверкой с mutation-gate.yml/frontend-job (byte-идентичный паттерн вычисления base) |
| AC5 (пустой отбор → зелёный выход без сборки бандла) | unit + чтение | test/mutation-gate.test.mjs:250 (#475 AC5) для отбора; guardNeedsBundle/buildBundle вызывается только для отобранных мутантов с браузерным гвардом (scripts/mutation-gate.mjs:6779,6801) — проверено чтением, не исполнением полного CI-прогона |
защита не заявлена как guard/limit — расположение раннего выхода, обычное сравнение |
AC6 (репродукция #467: src/wall-thickness.ts → 3 мутанта) |
unit | test/mutation-gate.test.mjs:255 (#475 AC6), прогнан в составе npm test |
простое сравнение множества id, мутант не требуется правилом (не защитный AC) |
AC7 (репродукция ревью: .py-патч/гард → 4 бэкенд-мутанта) |
unit (неполный, см. находку Low) + независимая проверка | test/mutation-gate.test.mjs:259; дополнено прямым вызовом selectChangedMutants в этом ревью, подтвердившим все 4 |
простое сравнение множества id |
Что проверено и корректно
guardFiles()извлекает файлы без парсинга команды: только токены вида[\w./-]+\.(mjs|py), не начинающиеся с-, существующие в репозитории. Проверено на реальных гардах (--test-name-pattern="magnet presses|x.mjs"корректно исключает фрагмент шаблона, оставляя толькоtest/furniture.test.mjs) и наbundle:sync && node demo/smoke_*.mjs(составные команды с&&).selectChangedMutantsобратно совместима: старые вызовы без третьего параметраexistsпродолжают работать (дефолт —existsSync), старый тест#332по-прежнему проходит без изменений сигнатуры вызова.- Мутанты #475 сгенерированы корректно: якорь
changed-selection-ignores-guard-filesсобран из двух конкатенированных строк ровно затем, чтобы--checkне находил его дважды (в коде и в определении мутанта) — воспроизведено:--checkдаётokна обоих новых мутантах. - Установка Python-зависимостей и Chromium в
changed_mutantsдословно повторяет шагиmutation-gate.yml(checkout → setup-node → npm ci → setup-python 3.14 →pip install -r tests_backend/requirements.txt→ кэш Playwright → установка Chromium при промахе кэша) — сверено построчно. - База диапазона (
PROVEN_BASE/BEFORE_SHA/merge-baseс фолбэком) побайтово идентична блоку, уже проверенному в jobfrontendдля гейта «новый код не добавляет any» (#387/#388) — не новая, а переиспользованная логика. - Трейлеры коммита:
Issue: #475,User-Visible: no— корректно для инфраструктурной правки без пользовательского эффекта; changelog не тронут, что верно приUser-Visible: no. - Класс файлов: все правки — класс B (
test/**,scripts/**,.github/workflows/**), issue переиспользован по правилу AGENTS.md.
Унаследовано из предыдущих раундов
Не применимо — это первый заход код-ревью (r1). Ревью ТЗ (SPEC-REVIEW-475-r1
жёлтый, SPEC-REVIEW-475-r2 зелёный) относится к отдельному этапу и не
экономит объём этого разбора; код проверен полностью, включая уже закрытую
там находку (расширение .py/backend), которая была сверена по факту в коде
(см. «Что проверено и корректно»), а не принята на слово.
Вердикт
Жёлтый. Единственная блокирующая находка — Medium в скоупе задачи
(триггер changed_mutants не покрывает изменение scripts/mutation-gate.mjs,
как того явно требует Contract §2 принятого ТЗ). High-находок нет. Правится
в этом же issue без нового цикла ревью ТЗ; после фикса — обычный повторный
заход код-ревью по дельте (§2.10).
Материал раунда
- Ветка:
issue/475-changed-mutants-on-push, коммит9b41ac1c7041— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
5483eee97c02fa1a40b6d6199b30741353263bf4git log --all --format='%H %T' | grep 5483eee97c02