Files
houseplan-card/docs/reviews/CODE-REVIEW-475-r1.md
T
2026-09-06 19:31:48 +03:00

188 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-475-r1
Issue: #475 · «mutation-gate --check не отличает живого свидетеля от мёртвого»
Трек: `small` (лёгкий) · Этап: код-ревью · Заход: r1 · блокирующих циклов 0/2
SHA материала: `c0207af40f25564eac9773d583cdcc7f5c56ea45` (`git rev-parse HEAD` перед выводом)
Ветка приведена к `dev` конвейером до ревью: поверх легло 4 коммит(ов) dev
(`9b41ac1c` → `c0207af4`). Диапазон `origin/dev..HEAD` после этого содержит
ровно один коммит задачи — разбор полный (§7.2), а не по дельте, как и
предписано при ребейзе на ушедший вперёд `dev`.
## Скоуп
Диапазон `git diff origin/dev...HEAD`, 4 файла, все класса B:
- `.github/workflows/validate.yml` — новая job `changed_mutants`;
- `scripts/mutation-gate.mjs` — `guardFiles()`, расширенный `selectChangedMutants()`, 2 новых мутанта;
- `test/mutation-gate.test.mjs` — AC1–AC3, AC5–AC7;
- `test/validate-workflow.test.mjs` — контрактный тест на YAML (AC4).
Класса A ни один файл не задет — трек `small`, ТЗ в теле issue, ревью ТЗ
прошло два захода (r1 жёлтый/Medium, r2 зелёный) и закрыто до кода.
## Как проверялось
Гейты, соразмерно классу B без изменений в `src/**` (issue #127, PROCESS.md §8):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный, 7.2s |
| Юниты | `npm test` | **2083 pass / 0 fail / 1 skip**, 38.7s (совпадает с числом автора в хендоффе) |
| Сборка + синхрон бандла | `npm run build` затем `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | зелёный, байт-в-байт совпадение |
| Новый код не добавляет `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | «Проверено добавленных строк в src/\*\*/\*.ts: 0» — диф не касается `src/**` |
| Отбор браузерных смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «Исполняемого frontend-диффа нет (src/\*\*/\*.ts не тронут)» — смоки не выбраны, выбирать нечего |
| Реестр мутантов применим | `node scripts/mutation-gate.mjs --check` | 509 мутантов, все `ok`, включая 2 новых |
| Мутант `changed-selection-ignores-guard-files` | `node scripts/mutation-gate.mjs --id=changed-selection-ignores-guard-files` | «чистый прогон: ok», «тест покраснел, как обязан», поймано 1/1 |
| Мутант `changed-selection-matches-any-token` | `node scripts/mutation-gate.mjs --id=changed-selection-matches-any-token` | то же, поймано 1/1 |
| Самоприменение отбора | `node scripts/mutation-gate.mjs --changed=origin/dev..HEAD` | «файлов в диффе 4, мутантов затронуто 4 из 509» — независимо воспроизведено, состав описан ниже |
| AC7 напрямую | `node -e "...selectChangedMutants(MUTANTS, [...])..."` | все 4 бэкенд-мутанта `frontend_registration.py`/`test_ha_frontend_registration.py` отобраны и по гарду, и по патчу |
**Не прогонялось и почему:**
- `check-docs.mjs` — правило #127/§8 требует его при диффе по `src/**`; diff файлов `src/**` не содержит. Пропуск честный, не молчаливый.
- `python -m pytest tests_backend -q` — окружение ревью не содержит `.venv-backend` и `pytest` (`pip show pytest` → not found), это задокументированное ограничение (PROCESS.md «Backend» и AGENTS.md «Environments»), не результат этой задачи. При попытке независимо прогнать `--changed=origin/dev..HEAD` до конца скрипт сам упёрся в это же: мутант `typing-gate-stops-running` (не из #475, патчит `.github/workflows/validate.yml`) требует `pytest`, которого здесь нет. AC7 проверен напрямую через API функции (см. таблицу) вместо прогона pytest.
- `golden:verify`, model-invariants, performance-профили, single-source-numbers — diff не меняет визуал, геометрию, ссылки на неё или пользовательские числа; неприменимо.
## Находки
### Medium (в скоупе) — триггер job не покрывает третье условие контракта
**Файл:** `.github/workflows/validate.yml:410`
**Что не так:** Контракт §2 принятого ТЗ (тело issue #475, редакция после фикса Medium из SPEC-REVIEW r1) требует:
> job запускается при `frontend == 'true'` **или `backend == 'true'` или изменении `scripts/mutation-gate.mjs`**
Реализовано:
```yaml
if: needs.changes.outputs.frontend == 'true' || needs.changes.outputs.backend == 'true'
```
Третье условие отсутствует — ни в `if:`, ни в контрактном тесте
`test/validate-workflow.test.mjs:290`, который проверяет ровно эти же два
дизъюнкта и не более.
**Воспроизведение (не гипотеза — проверено чтением классификатора):**
`changes` job классифицирует файлы двумя regex (`validate.yml:262-263`):
```
frontend: ^(src/|demo/|test/|dist/|custom_components/houseplan/frontend/|package(-lock)?\.json$|rollup\.config\.mjs$|tsconfig)
backend: ^(custom_components/.*\.py$|tests_backend/|scripts/support-relay/|pytest\.ini$)
```
`scripts/mutation-gate.mjs` не начинается ни с одного из перечисленных
префиксов и не совпадает ни с одним `$`-якорем — ни `frontend`, ни `backend`
не станут `true` от диффа, ограниченного этим файлом. Значит правка
`scripts/mutation-gate.mjs` (например, новый мутант, патчащий
`src/`/`custom_components/`-файл, без сопутствующей правки в `test/**`)
проходит без единого запуска `changed_mutants` — job попросту не
запланируется на этом пуше.
Это ровно тот класс дефекта, ради которого заведён #475: рефакторинг файла,
отвечающего за отбор мутантов, остаётся невидимым до недельного полного
прогона (или до релиза) — тот же разрыв «неделя–релиз» из #466/#467, только
теперь применительно к самому гейту, а не к продуктовому коду. Contract §2
называет это условие явно и с тем же обоснованием, каким объяснено условие
`backend` («бэкенд-мутанты патчат `.py`... дифф... даёт `backend=true` без
`frontend=true`») — то есть это не пропущенная деталь ТЗ, а нереализованная
часть уже принятого контракта.
**Почему не High:** основной сценарий (правка продуктового/тестового кода,
меняющая `patch.file` или файл гарда любого мутанта) работает и подтверждён
и юнитами, и самоприменением. Пробел узкий — правка ограничена буквально
одним файлом `scripts/mutation-gate.mjs` без сопутствующих `test/**`/`src/**`/
`custom_components/**.py` изменений — но контракт именно этот случай называет
по имени, поэтому строка контракта не выполнена.
**Чем закрывается (в скоупе, без выхода за задачу):** добавить в `if:`
третий дизъюнкт, обнаруживающий изменение `scripts/mutation-gate.mjs`
(например, отдельным шагом `changes`/`classify` либо прямой проверкой пути
диапазона в самой job), и покрыть его тем же контрактным тестом.
### Low — тест AC7 не покрывает четвёртый бэкенд-мутант, хотя код это делает
**Файл:** `test/mutation-gate.test.mjs:259–267`
В реестре ровно 4 мутанта, патчащих `custom_components/houseplan/frontend_registration.py`
с гардом `tests_backend/test_ha_frontend_registration.py` (`scripts/mutation-gate.mjs:120,132,146,158`).
Тест AC7 фильтрует их через `id.startsWith('frontend-registration-')`, что
даёт только 3 — четвёртый называется `frontend-reload-notice-forgets-persisted-flag`
(другой префикс id, тот же патч-файл и гард) и в assert-цикл не попадает.
Проверено напрямую (не через тест): `selectChangedMutants` реально отбирает
все 4 и по гарду, и по патчу — сама реализация не различает мутанты по имени,
поэтому регрессии здесь нет, только тест не проверяет ровно то, что заявляет
ТЗ («отбирает все четыре frontend-registration-\* мутанта»). Наименование в
ТЗ тоже неточно (не все четыре носят префикс `frontend-registration-`).
Снимаю без исправления: механизм общий, не завязан на имя мутанта, и
пропущенный четвёртый защищён тем же кодовым путём, что и проверенные три —
регрессия, ломающая только его и не ломающая остальные три, потребовала бы
отдельного условия по конкретному id, которого в реализации нет.
## Проверка AC — таблица «чем доказан / чем краснеет»
| AC | Доказано | Чем | Чем краснеет |
|---|---|---|---|
| AC1 (патч-файл в диффе → отбор) | unit | `test/mutation-gate.test.mjs:228` (`#475 AC1`) + ранее существовавший `#332` тест | защитный AC без мутанта не заявлен (простое сравнение выборки) |
| AC2 (файл гарда — тест/смок/pytest — в диффе → отбор) | unit + мутант | `test/mutation-gate.test.mjs:233` (`#475 AC2`) | мутант `changed-selection-ignores-guard-files`: гвард (`--test-name-pattern="#475 AC2"`) чист на исходном коде и красный на патче (проверено: `--id=changed-selection-ignores-guard-files` → «поймано 1 из 1») |
| AC3 (токены-нефайлы не считаются) | unit + мутант | `test/mutation-gate.test.mjs:241` (`#475 AC3`) | мутант `changed-selection-matches-any-token`: проверено аналогично, «поймано 1 из 1» |
| AC4 (job в validate.yml: триггер, база диапазона, python-зависимости, блокирующая) | частично unit, частично чтением | `test/validate-workflow.test.mjs:283` + чтение `validate.yml:407-459` | **не выполнено полностью** — триггер не покрывает третий дизъюнкт контракта (см. находку Medium); база диапазона, установка Python и отсутствие `continue-on-error` подтверждены и текстом job, и сверкой с `mutation-gate.yml`/`frontend`-job (byte-идентичный паттерн вычисления `base`) |
| AC5 (пустой отбор → зелёный выход без сборки бандла) | unit + чтение | `test/mutation-gate.test.mjs:250` (`#475 AC5`) для отбора; `guardNeedsBundle`/`buildBundle` вызывается только для отобранных мутантов с браузерным гвардом (`scripts/mutation-gate.mjs:6779,6801`) — проверено чтением, не исполнением полного CI-прогона | защита не заявлена как guard/limit — расположение раннего выхода, обычное сравнение |
| AC6 (репродукция #467: `src/wall-thickness.ts` → 3 мутанта) | unit | `test/mutation-gate.test.mjs:255` (`#475 AC6`), прогнан в составе `npm test` | простое сравнение множества id, мутант не требуется правилом (не защитный AC) |
| AC7 (репродукция ревью: `.py`-патч/гард → 4 бэкенд-мутанта) | unit (неполный, см. находку Low) + независимая проверка | `test/mutation-gate.test.mjs:259`; дополнено прямым вызовом `selectChangedMutants` в этом ревью, подтвердившим все 4 | простое сравнение множества id |
## Что проверено и корректно
- **`guardFiles()`** извлекает файлы без парсинга команды: только токены вида
`[\w./-]+\.(mjs|py)`, не начинающиеся с `-`, существующие в репозитории.
Проверено на реальных гардах (`--test-name-pattern="magnet presses|x.mjs"`
корректно исключает фрагмент шаблона, оставляя только `test/furniture.test.mjs`)
и на `bundle:sync && node demo/smoke_*.mjs` (составные команды с `&&`).
- **`selectChangedMutants`** обратно совместима: старые вызовы без третьего
параметра `exists` продолжают работать (дефолт — `existsSync`), старый тест
`#332` по-прежнему проходит без изменений сигнатуры вызова.
- **Мутанты #475 сгенерированы корректно**: якорь `changed-selection-ignores-guard-files`
собран из двух конкатенированных строк ровно затем, чтобы `--check` не находил
его дважды (в коде и в определении мутанта) — воспроизведено: `--check` даёт
`ok` на обоих новых мутантах.
- **Установка Python-зависимостей и Chromium** в `changed_mutants` дословно
повторяет шаги `mutation-gate.yml` (checkout → setup-node → npm ci →
setup-python 3.14 → `pip install -r tests_backend/requirements.txt` → кэш
Playwright → установка Chromium при промахе кэша) — сверено построчно.
- **База диапазона** (`PROVEN_BASE`/`BEFORE_SHA`/`merge-base` с фолбэком)
побайтово идентична блоку, уже проверенному в job `frontend` для гейта
«новый код не добавляет any» (#387/#388) — не новая, а переиспользованная
логика.
- **Трейлеры коммита**: `Issue: #475`, `User-Visible: no` — корректно для
инфраструктурной правки без пользовательского эффекта; changelog не тронут,
что верно при `User-Visible: no`.
- **Класс файлов**: все правки — класс B (`test/**`, `scripts/**`,
`.github/workflows/**`), issue переиспользован по правилу AGENTS.md.
## Унаследовано из предыдущих раундов
Не применимо — это первый заход код-ревью (`r1`). Ревью ТЗ (`SPEC-REVIEW-475-r1`
жёлтый, `SPEC-REVIEW-475-r2` зелёный) относится к отдельному этапу и не
экономит объём этого разбора; код проверен полностью, включая уже закрытую
там находку (расширение `.py`/`backend`), которая была сверена по факту в коде
(см. «Что проверено и корректно»), а не принята на слово.
## Вердикт
Жёлтый. Единственная блокирующая находка — Medium в скоупе задачи
(триггер `changed_mutants` не покрывает изменение `scripts/mutation-gate.mjs`,
как того явно требует Contract §2 принятого ТЗ). High-находок нет. Правится
в этом же issue без нового цикла ревью ТЗ; после фикса — обычный повторный
заход код-ревью по дельте (§2.10).
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/475-changed-mutants-on-push`, коммит `9b41ac1c7041` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `5483eee97c02fa1a40b6d6199b30741353263bf4`
```
git log --all --format='%H %T' | grep 5483eee97c02
```