mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
166 lines
14 KiB
Markdown
166 lines
14 KiB
Markdown
# 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 → в задаче · Документ: (этот файл)
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `dev`, коммит `` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||
- Якоря снять не удалось: ветки задачи нет, материал читался по `dev`.
|