diff --git a/docs/reviews/SPEC-REVIEW-475-r2.md b/docs/reviews/SPEC-REVIEW-475-r2.md new file mode 100644 index 00000000..c314fd1d --- /dev/null +++ b/docs/reviews/SPEC-REVIEW-475-r2.md @@ -0,0 +1,178 @@ +# 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`.