14 KiB
SPEC-REVIEW — issue #475 · заход r1
Тема: mutation-gate --check не отличает живого свидетеля от мёртвого —
добавить отбор мутантов по диффу («гард») и запуск на каждом пуше.
Трек: лёгкий (small). ТЗ живёт в теле issue (комментарий «ТЗ готово»,
2026-09-06T10:54:23Z). Файла в docs/specs/ нет и не должно быть.
Класс изменения: B (гейты и инструменты) — scripts/mutation-gate.mjs,
test/mutation-gate.test.mjs, .github/workflows/validate.yml. Ни одного
файла класса A не затронуто.
Скоуп
Расширить selectChangedMutants так, чтобы мутант считался задетым диффом не
только по patch.file, но и по файлам своего гарда (тест/смок), и подключить
--changed как блокирующий job validate.yml на каждом пуше. Цель — сократить
разрыв между поломкой свидетеля и её обнаружением с «неделя/релиз» до «тот же
пуш» (мотивирующий инцидент — #466/#467).
docs/SCOPE.md этот класс задач не ограничивает: это не пользовательская
функция ни для одной из персон, а инструмент инженерного качества (по типу
042-backend-engineering-quality.md); видимого поведения продукта нет, что и
делает трек лёгким и ставит его вне вопроса «какую строку Core user jobs
закрывает». docs/USER-GUIDE.ru.md неприменим — интерфейса нет.
Как проверялось
Прочитано: docs/SCOPE.md, AGENTS.md, PROCESS.md целиком, тело issue #475
и оба комментария (S2-анализ и ТЗ). Код читался, не исполнялся:
scripts/mutation-gate.mjs— существующиеselectChangedMutants,shardMutants, ветка--changedвmain(), весьMUTANT_DEFINITIONS(60+ мутантов, все вариантыguard:);.github/workflows/validate.yml— jobchanges(классификацияfrontend/backend/integration, выводbase/range_base), jobfrontend(if: needs.changes.outputs.frontend == 'true'), jobbackend(if: needs.changes.outputs.backend == 'true' ...);- подтверждено, что
test/validate-workflow.test.mjsуже существует — «контрактный тест на YAML», на который ссылается доказательство AC4, не изобретённая сущность, а существующий паттерн.
Гейты не гонялись: диапазон материала — только тело issue, продуктового кода
нет, typecheck/test/build к тексту ТЗ неприменимы на этой стадии.
Находки
Medium (в скоупе задачи) — job и guardFiles() не покрывают гарды бэкенда, хотя они уже есть в реестре
Файл: тело issue #475, разделы «1. Отбор по диффу…» и «2. --changed
гоняется на каждом пуше».
Контракт заявлен без ограничения по подсистеме: «мутант считается затронутым,
если дифф содержит любой patch.file или любой файл, на который
ссылается его guard» — и мотивирующий сценарий («тест перестал ходить по
мутированной ветке») сформулирован в S2-разборе тоже без оговорки
«только фронтенд». Но два независимых сужения в самом же контракте молча
исключают уже существующие бэкенд-мутанты:
- Извлечение файлов гарда ограничено суффиксами
.mjs/.test.mjs. В реестре (scripts/mutation-gate.mjs:121-159) четыре мутанта (frontend-registration-skips-retry,frontend-registration-retries-without-delay,frontend-registration-is-not-unload-bound,frontend-reload-notice-forgets-persisted-flag) патчатcustom_components/houseplan/frontend_registration.pyи охраняются напрямуюpython3 -m pytest tests_backend/test_ha_frontend_registration.py -k "..."— токенtests_backend/test_ha_frontend_registration.pyоканчивается на.pyи по объявленному правилу файлом гарда не считается вовсе. - Триггер job —
needs.changes.outputs.frontend == 'true'(плюс правка самогоmutation-gate.mjs). Паттернfrontendв jobchanges(.github/workflows/validate.yml:281) —^(src/|demo/|test/|dist/| custom_components/houseplan/frontend/|package(-lock)?\.json$| rollup\.config\.mjs$|tsconfig)— не включает ниtests_backend/, ниcustom_components/houseplan/frontend_registration.py(последний попадает под отдельный паттернbackend,^(custom_components/.*\.py$|tests_backend/|...), строка 282). Диапазон диффа, задевающий толькоcustom_components/houseplan/frontend_registration.pyи/илиtests_backend/test_ha_frontend_registration.py, выставляетbackend=true, но неfrontend=true— и по объявленному условию job «Мутанты по диффу» вообще не запустится.
Воспроизведение (по коду, без запуска): диапазон, меняющий строку
entry.async_on_unload(state.cancel) на pass в
custom_components/houseplan/frontend_registration.py и одновременно
ослабляющий соответствующую проверку в
tests_backend/test_ha_frontend_registration.py (буквально сценарий
«гард перестал ходить», ради которого написан этот контракт) — задевает
только паттерн backend. changes.outputs.frontend остаётся false, job
«Мутанты по диффу» не запускается (условие в п.2 ТЗ), значит ни AC1, ни
предполагаемый AC2 для этого мутанта не проверяются ни на одном пуше — свежая
поломка живого свидетеля здесь снова обнаружится только полным еженедельным
прогоном, то есть именно тот разрыв, ради закрытия которого заведён #475,
для этой части реестра остаётся ровно тем же, что и до задачи.
AC6 воспроизводит только фронтенд-случай (src/wall-merge.ts,
multi-wall-*), поэтому по документу проверить это не на чем — а
существующая часть реестра, для которой контракт молча не работает, реальна
и приведена выше, не гипотетична.
Это не продуктовый вопрос — граница чисто техническая (какое условие триггера
job, какие суффиксы допускает guardFiles), значит по §7.1 её решает автор, а
не владелец. Но граница должна быть решена явно — либо расширить условие
триггера до frontend == 'true' || backend == 'true' || mutation-gate.mjs изменён и расширить извлечение токенов гарда суффиксом .py, либо, если
бэкенд-гарды сознательно остаются вне этой задачи, написать это прямо рядом с
уже объявленной границей про фикстуры — сейчас в тексте объявлена только
одна граница (фикстуры), а фактическая шире.
Чем закрывается: AC2 (или новый AC) должен явно называть исход для бэкенд-патченных/бэкенд-охраняемых мутантов, и триггер job — соответствовать этому исходу. Один из двух путей выше, зафиксированный в тексте issue.
Что проверено и корректно
- Обязательные для лёгкого трека разделы (§5): проблема · контракт · AC1…AC6 с доказательством · откат — все присутствуют, ни один не пропущен.
- Каждый AC однозначен и называет способ доказательства (unit / контрактный
тест на YAML), доказательство реалистично:
test/validate-workflow.test.mjsдля проверки структурыvalidate.ymlуже существует как паттерн (AC4). - База диапазона
needs.changes.outputs.base/range_base(#387/#388) — существующая инфраструктура, а не изобретённая; подтверждена и в коде (.github/workflows/validate.yml:182-190), и в S2-комментарии владельца. - Отказ от «отметки последнего доказательства» из исходного текста issue — явное техническое решение с приведённой причиной (кому обновлять отметку), не догадка, выданная за факт.
- Граница по фикстурам гарда (
test/fixtures/*не выводятся из команды) — объявлена прямо и признана самим автором как известное ограничение; это не находка, а корректно зафиксированное решение. - Ранний зелёный выход при пустой выборке — уже реализован в
main()/--changed(scripts/mutation-gate.mjs, веткаif (!selected.length)до вызоваrunCleanGuards), так что AC5 доказуем без новой логики. - Критерии лёгкого трека (§5) не нарушены: сложность ≤3 по факту одной логической поверхности (отбор мутантов + его подключение в CI), нет миграции конфига, нет нового UX-контракта (контракта UX не существует — инструмент внутренний), нет влияния на touch; влияние на длительность прогона названо и посчитано приемлемым (S2-комментарий).
- Продуктовый код не затронут ни одним файлом (класс A отсутствует), значит вопрос «какую строку Core user jobs закрывает» к этой задаче неприменим по типу задачи, а не пропущен.
Чего не проверял
- Не запускал
node scripts/mutation-gate.mjs --changed=...и не писал сам тест — на стадии ревью ТЗ кода ещё нет, это предмет код-ревью (§2.7). - Не проверял полный список всех 60+ мутантов на предмет других суффиксов
гарда, отличных от
.mjs/.test.mjs/.py(например, составные команды сtsc/fix-test-build.mjs) — выборочно просмотрел реестр целиком черезgrep, других расширений файлов-целей, кроме.mjs,.test.mjsи.py, не встретил. - Не оценивал производительность самого job на реальном CI (стоимость сборки бандла при непустой выборке) — S2-комментарий называет её «минуты», и это вне ревью ТЗ, оценка возможна только по факту в код-ревью.
Вердикт
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/2 · High: 0 · Medium: 1 → в задаче · Документ: (этот файл)
Материал раунда
- Ветка:
dev, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Якоря снять не удалось: ветки задачи нет, материал читался по
dev.