# SPEC-REVIEW — issue #475 · заход r2 **Тема:** `mutation-gate --check` не отличает живого свидетеля от мёртвого — добавить отбор мутантов по диффу («гард») и запуск на каждом пуше. **Трек:** лёгкий (`small`). ТЗ живёт в теле issue. Файла в `docs/specs/` нет и не должно быть. **Класс изменения:** B (гейты и инструменты) — `scripts/mutation-gate.mjs`, `test/mutation-gate.test.mjs`, `.github/workflows/validate.yml`. Ни одного файла класса A не затронуто. Это второй заход ревью ТЗ. Первый (r1, документ `docs/reviews/SPEC-REVIEW-475-r1.md`, коммит `f52b2449`) дал жёлтый вердикт: 1 Medium в скоупе, 0 High. Бюджет циклов лёгкого трека — 2, зелёный вердикт бюджет не тратит (#227), поэтому на входе в r2 израсходован 1 из 2. ## Материал этого раунда Разбор — по дельте (PROCESS.md §2.10), не заново. Предмет — изменения тела issue между комментарием «ТЗ готово» (2026-09-06T10:54:23Z, материал r1) и текущим телом issue на момент этого раунда. Спецификация лёгкого трека живёт в теле issue, а не в файле репозитория, и `git diff ..HEAD` к ней неприменим в принципе — тело issue не версии git. Документ r1 сам это фиксирует в блоке «Материал раунда»: «ветка `dev`, коммит `` — ... Якоря снять не удалось: ветки задачи нет, материал читался по `dev`». Поле SHA там пустое не потому, что кто-то забыл его заполнить или подменил значение перед выводом (это и было бы находкой по §2.10), а потому, что на стадии ревью ТЗ лёгкого трека веток и коммитов задачи ещё не существует — код не написан. Отмечаю это как наблюдение по механике шаблона документа, а не как находку против самого ТЗ: дельта тела issue устанавливается надёжно и без git SHA — через точную цитату исходного текста в документе r1 (раздел «Находки», кавычки контракта и AC-таблицы) и явный комментарий владельца «Medium исправлен» (2026-09-06T11:02:13Z), построчно перечисляющий, что изменено. Я сверил оба источника с текущим телом issue дословно (см. ниже) — расхождений между заявленным и фактическим текстом нет. ## Закрытие раунда r1 | Находка r1 | Чем закрыта | Где это видно | |---|---|---| | Medium: извлечение файлов гарда ограничено суффиксами `.mjs`/`.test.mjs` — токен `tests_backend/test_ha_frontend_registration.py` (`.py`) файлом гарда не считался, 4 бэкенд-мутанта `frontend-registration-*` выпадали из отбора | Список суффиксов расширен до `.mjs`, `.test.mjs`, **`.py`** | Тело issue, раздел «1. Отбор по диффу…»: «все токены команды, оканчивающиеся на `.mjs`, `.test.mjs` **или `.py`**… Бэкенд-мутанты (`frontend-registration-*`... покрываются наравне с фронтендовыми» | | Medium: триггер job — только `frontend == 'true'`; паттерн `frontend` в `changes` не включает `tests_backend/` и `custom_components/houseplan/frontend_registration.py` (это `backend`) — диапазон, меняющий только эти файлы, не запускал job вовсе | Триггер расширен до `frontend == 'true' \|\| backend == 'true'` (плюс правка `mutation-gate.mjs`) | Тело issue, раздел «2. `--changed` гоняется на каждом пуше»: «запускается при `frontend == 'true'` **или `backend == 'true'`** или изменении `scripts/mutation-gate.mjs` — бэкенд-мутанты патчат `.py` и охраняются pytest, и дифф только по `custom_components/…/frontend_registration.py` и `tests_backend/` даёт `backend=true` без `frontend=true`» — то есть новый триггер прямо называет прежде пропущенный случай | | (следствие находки) отсутствовал AC, доказывающий закрытие именно этого сценария | Добавлен **AC7** | Таблица AC, строка: «AC7 \| Воспроизведение находки ревью: дифф только из `custom_components/houseplan/frontend_registration.py` и `tests_backend/test_ha_frontend_registration.py` отбирает все четыре `frontend-registration-*` мутанта \| unit по реальному реестру» | | (сопутствующее) job не ставил Python-зависимости — без них бэкенд-гварды (`pytest`) неисполнимы даже при верном отборе | В job добавлена установка Python и `tests_backend/requirements.txt`, тем же способом, что в `mutation-gate.yml` | Тело issue, раздел 2: «Job ставит Python и `tests_backend/requirements.txt` так же, как `mutation-gate.yml`, чтобы pytest-гварды были исполнимы» | | (сопутствующее) граница по фикстурам называла только `test/fixtures/*`, хотя бэкенд-мутанты получили свою фикстурную директорию | Граница расширена на `tests_backend/fixtures/*` | Тело issue, раздел 1, последнее предложение: «Фикстуры гардов (`test/fixtures/*`, `tests_backend/fixtures/*`) по команде не выводятся и отбором не покрываются» | ## Как проверялась дельта Не поверил формулировке фикса на слово — перечитал реальную инфраструктуру, на которую ссылается новый текст, потому что именно это и было предметом находки r1 (техническая граница, а не догадка): - `.github/workflows/validate.yml:281-282` — паттерн `backend`: `^(custom_components/.*\.py$|tests_backend/|scripts/support-relay/| pytest\.ini$)`. Подтверждено: и `custom_components/houseplan/ frontend_registration.py`, и `tests_backend/test_ha_frontend_registration.py` попадают именно под него, а не под `frontend` — сценарий находки воспроизводится буквально, и добавление `backend == 'true'` в триггер закрывает его именно так, как описано в тексте; - `scripts/mutation-gate.mjs:120-164` — реестр действительно содержит ровно 4 мутанта `frontend-registration-*`, `patch.file` каждого — `custom_components/houseplan/frontend_registration.py`, `guard` каждого — `python3 -m pytest tests_backend/test_ha_frontend_registration.py …`. Число «четыре» в AC7 и в комментарии владельца не выдумано; - `scripts/mutation-gate.mjs:6800-6802` — `selectChangedMutants` на сегодня фильтрует **только** по `patch.file` (`m.patches.some((patch) => changed.has(patch.file))`), гард не смотрит вовсе. Это подтверждает исходную формулировку проблемы («второй способ сгнить» — правка гарда без правки патча — сегодня действительно не покрыт ничем) и то, что расширение до guard-файлов — не косметика, а реальный новый путь отбора; - `.github/workflows/validate.yml:181-190` — job `changes` действительно экспортирует `base` и отдельно `range_base` (разные имена нарочно, для PR и для `dev`), как и заявлено в тексте issue; - `.github/workflows/mutation-gate.yml` — шаг «Установить backend test dependencies» (`actions/setup-python@v7` + `pip install -r tests_backend/requirements.txt`) существует и именно так и называется «тем же способом» в тексте issue — не изобретённый паттерн; - `scripts/mutation-gate.mjs` — ранний зелёный выход при пустой выборке (`if (!selected.length) { …; return 0; }` в `main()`) существует уже сегодня — AC5 (не тронут дельтой) остаётся доказуемым без новой логики, как и было установлено в r1. Гейты кода не гонялись — на стадии ревью ТЗ лёгкого трека продуктового кода (в терминах этой задачи — кода гейта) ещё нет, дельта целиком лежит в тексте issue; это симметрично тому, что зафиксировал r1. ## Находки Нет. Единственная находка r1 (Medium, в скоупе) закрыта корректно и полно — см. таблицу выше и её техническую верификацию. Новых противоречий, недоказуемых AC или догадок, выданных за факт, дельта не вносит: оба изменённых раздела и новый AC7 опираются на подтверждённую существующую инфраструктуру (составы `backend`/`frontend` в `changes`, реальный реестр мутантов, существующий приём установки Python-зависимостей в `mutation-gate.yml`), а не на предположение. Отдельно проверено, не сломал ли фикс что-то из уже принятого: расширение списка суффиксов до `.py` не расширяет отбор ложно — правило по-прежнему требует, чтобы токен совпадал с **существующим в репозитории файлом** (AC3), поэтому произвольный `.py`-аргумент команды (флаг, шаблон) в отбор не попадёт; добавление `backend` в триггер не запускает job на диффах, не трогающих ни `frontend`, ни `backend` — правило OR только расширяет множество, ранее пропущенное, и не сужает уже работавшую фронтенд-ветку. ## Унаследовано из r1 Ссылка: `docs/reviews/SPEC-REVIEW-475-r1.md`, коммит `f52b2449` (по дереву `dev` на момент r1 — веток задачи нет, см. «Материал этого раунда» выше). Принято без повторной проверки в этом раунде, так как дельта их не затрагивает: - обязательные для лёгкого трека разделы (§5) — проблема · контракт · AC с доказательством · откат — присутствуют, ни один не пропущен; дельта добавила один AC (AC7) и расширила два раздела, структуру не меняла; - AC1, AC3, AC5, AC6 — формулировки не менялись дельтой, доказуемость, установленная в r1, наследуется как есть (AC5 при этом попутно переподтверждён чтением кода — см. выше, без изменения вывода); - отказ от «отметки последнего доказательства» из исходного текста issue — явное техническое решение с причиной, дельтой не затронуто; - критерии лёгкого трека §5 (сложность ≤3, одна поверхность, без миграции конфига, без нового UX-контракта, без влияния на touch) — дельта добавляет один суффикс расширения и одно условие OR в YAML, порядок сложности не меняет; вывод r1 «критерии не нарушены» остаётся в силе; - продуктовый код не затронут ни одним файлом (класс A отсутствует) — дельта тоже класса A не касается. ## Что проверено и корректно (сверх унаследованного) - Дельта строго соответствует тому, что заявил комментарий владельца «Medium исправлен»: все четыре перечисленных им изменения (суффикс `.py`, условие `backend`, установка Python-зависимостей, AC7, расширение границы фикстур) присутствуют в теле issue дословно. - Выбранный путь закрытия («расширить, а не исключить», второй из двух предложенных r1 вариантов) технически состоятелен: подтверждено чтением `validate.yml` и `mutation-gate.yml`, а не принято на слово. - Новый AC7 — не декоративный: он называет конкретное число («все четыре»), которое совпадает с фактическим размером подмножества `frontend- registration-*` в реестре, и явно завязан на реальные пути (`custom_components/houseplan/frontend_registration.py`, `tests_backend/test_ha_frontend_registration.py`), а не абстрактный случай. ## Чего не проверял - Не читал остальные 55+ мутантов реестра повторно на предмет суффиксов гарда, отличных от `.mjs`/`.test.mjs`/`.py` — в r1 такая выборочная проверка уже была сделана и её вывод («других суффиксов не встречено») дельтой не оспорен, поэтому не переделывал. - Не запускал `node scripts/mutation-gate.mjs --changed=...` и не писал сам тест — кода ещё нет, это предмет код-ревью (§2.7), как и в r1. - Не оценивал фактическую длительность нового job на CI при непустой выборке для бэкенд-гвардов (установка Python + pytest) — числовая оценка осталась на уровне S2-комментария владельца («минуты»); проверка по факту возможна только на код-ревью. ## Вердикт Вердикт: зелёный · заход r2 · блокирующих циклов 1/2 · High: 0 · Medium: 0 Единственная находка предыдущего раунда закрыта полно и технически корректно; дельта не вносит новых находок и не ставит под сомнение AC, унаследованные из r1. Готово к разработке. --- ## Материал раунда --- ## Материал раунда - Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Якоря снять не удалось: ветки задачи нет, материал читался по `dev`.