Files
houseplan-card/docs/reviews/SPEC-REVIEW-475-r1.md
T
2026-09-06 11:00:53 +00:00

14 KiB
Raw Blame History

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 — job changes (классификация frontend / backend / integration, вывод base/range_base), job frontend (if: needs.changes.outputs.frontend == 'true'), job backend (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-разборе тоже без оговорки «только фронтенд». Но два независимых сужения в самом же контракте молча исключают уже существующие бэкенд-мутанты:

  1. Извлечение файлов гарда ограничено суффиксами .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 и по объявленному правилу файлом гарда не считается вовсе.
  2. Триггер job — needs.changes.outputs.frontend == 'true' (плюс правка самого mutation-gate.mjs). Паттерн frontend в job changes (.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.