Files
houseplan-card/docs/reviews/CODE-REVIEW-42-r2.md
T
2026-08-30 21:34:32 +03:00

232 lines
21 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-42-r2
- Issue: https://github.com/Matysh/houseplan-card/issues/42
- Ветка: `issue/42-backend-quality`, HEAD на момент вердикта: `7e5a19376d921ba0faf9929ff040e467d0e8c0d0`
- Заход: r2 (второй фактический прогон ревью кода; счётчик циклов §4 = 1/4,
зелёного цикла не образуется, а два комментария «ревью не запускалось» из-за
конфликта ребейза — это остановка конвейера ДО ревью, не цикл)
- Материал: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`
(53 файла, 926+/321-)
## Почему разбор полный, а не по дельте (§2.10)
Между r1 (SHA `ab337193`) и этим заходом ветка **дважды ребейзилась** на ушедший
вперёд `dev` (конфликты в собранных бандлах, зафиксированы в issue дважды —
прогоны 33318210024 и 33318796499). §7.2/§2.10 прямо называют этот случай
границей: «после ребейза это другой код». Поэтому ниже — полный разбор всех
пяти блоков ТЗ (ревизия 6), а не только диффа поверх r1.
## Закрытие раунда r1
r1 (SHA `ab337193abb8c629322313a0bf2d747105942db4`) был красным: High: 2.
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
| **H1** — новый `any` в `src/houseplan-card.ts:9743` (`(item: any)`) | Заменено на `(item: { id?: unknown })` | `src/houseplan-card.ts:9757` (текущий HEAD); `node scripts/no-new-any.mjs --base origin/dev --head HEAD` → «Новых any нет» — перепроверено мной командой сейчас |
| **H2** — `requirements_test.txt` не разрешим (`pytest==9.1.1`/`pytest-cov==7.1.0` конфликтуют с транзитивными пинами `pytest-homeassistant-custom-component`) | Пины сняты, затем `pytest-homeassistant-custom-component` закреплён на `0.13.316` (последний релиз с `Requires-Python <3.14`) в коммитах `9a62182f`→`3b1ccb85`→`5b102471` | Backend CI job HEAD `7e5a19376d92...` (job 99276949757): `pip install -r requirements_test.txt` завершается `Successfully installed ... pytest-homeassistant-custom-component-0.13.316 ... homeassistant-2026.2.3` — проверено чтением лога прогона, не со слов автора |
Оба High закрыты по существу, независимо перепроверено исполнением/логом CI, а
не по формулировке автора.
## Унаследовано из r1
Ничего не наследуется без повторной проверки в этом раунде — ребейз на ушедший
вперёд `dev` обязывает к полному разбору (см. выше), поэтому все AC ниже
перепроверены заново на текущем HEAD, а не приняты по r1. Единственное, что
буквально не пересчитывалось заново, а взято как решённое владельцем и не
подлежащее пересмотру ревьюером кода: сам текст ТЗ ревизии 6 (арбитраж
владельца от 30.08, бюджет ревью ТЗ исчерпан 4/4) — согласно роли (§6),
код-ревью не ревьюит спецификацию повторно, только реализацию.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы (frontend) | `npx tsc --noEmit` | чисто |
| Юниты (frontend) | `npm test` | 1636 pass / 0 fail / 1 skipped (совпадает с хендоффом) |
| Сборка + сверка 3 копий бандла | `npm run bundle:sync` | без diff после сборки — dist/custom_components/demo идентичны |
| Бюджет бандла | `npm run bundle:budget` | initial View 282745 B gzip / 300000 (запас 17255 B; авторские 282738 — расхождение в 7 байт, не критично) |
| `no-new-any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | новых `any` нет (H1 подтверждён закрытым) |
| **Доки (`docs`, реальный блокер по AGENTS.md)** | `node scripts/check-docs.mjs --external` | **КРАСНО** — 10 несовпадений хэшей скриншотов (см. High-1) |
| ruff (CI-скоуп: только `custom_components/houseplan`) | `python -m ruff check custom_components/houseplan` | чисто |
| ruff (полный `include` из pyproject: + `scripts`, `tests_backend`) | `python -m ruff check custom_components/houseplan scripts tests_backend` | 53 находки — но CI это никогда не проверяет (см. Low-3) |
| mypy strict (6 модулей allowlist) | `python -m mypy --config-file pyproject.toml -p ...` (все 6 модулей) | `Success: no issues found in 6 source files` |
| Контракт-тесты (pure pytest) | `python -m pytest tests_backend/test_backend_quality.py -q` | 3 passed; мутант «убрать `invalid_passage_fields` из ERROR_CODES» — воспроизведён вручную, тест падает (AssertionError), файл восстановлен |
| Фронт-юниты error-текста (AC6) | `node --test test/open-passage-contract.test.mjs` | 8/8, включая тест `_errText` JSON-first/code-first fallback |
| mutation-gate self-test | `node --test test/mutation-gate.test.mjs` | 10/10 |
| i18n-паритет `backup.error.*` | `git diff origin/dev...HEAD -- src/i18n/*.json` + подсчёт | ровно 22 новых ключа в каждом из 4 словарей |
| CI на точном SHA `7e5a1937` | `gh run view` / job logs | **красный**: `backend` (86 failed, унаследовано с `dev` — см. ниже), `Предполётные проверки` (провален `check-docs`, см. High-1); фронтенд/HACS/hassfest — зелёные |
| `smoke-select.mjs` | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «НЕОПРЕДЕЛЁННОСТЬ» — 1 изменённая строка `src/**`, символ `_serverCfg` слишком широкий, доказанной связи нет |
| Геометрия (`wall_segment_model.py`, B023-фиксы) | чтение диффа + backend CI job (строка покрытия `wall_segment_model.py … 87%`, модуль не в списке 86 упавших) | closures (`covered_by`/`eligible`/`materialize`/`pick`) вызываются в той же итерации, где определены — биндинг по умолчанию не меняет поведение; независимо подтверждено прогоном CI (не только чтением) |
| `junction_limits.py` | чтение диффа | только type hints + перестановка импортов, поведение не менялось (подтверждает вывод r1) |
## Находки
### High-1 — `docs`-гейт красный: манифест скриншотов не совпадает ни с одним закоммиченным файлом
Скоуп: в задаче (файл `docs/images/screenshots.json` — часть коммитов этой
ветки).
**Воспроизведение:**
```
node scripts/check-docs.mjs --external
```
даёт 10 ошибок вида `ERROR screenshot <id>: image hash does not match manifest`
для `view-desktop`, `view-touch`, `space-create`, `room-contour-close`,
`plan-context-tray`, `device-editor`, `device-display-preview`,
`background-editor`, `room-card`, `device-info`.
Причина: коммиты `3b1ccb85` («refresh the doc capture») и `7e5a1937` («the
doc capture is retaken on the rebased HEAD») переписали
`docs/images/screenshots.json` — обновили `sourceFingerprint`, `oxipng`
(`"oxipng 10.2.0"` → `null`) и `imageSha256` всех 10 сценариев на новые
значения — но **ни разу не тронули ни один PNG-файл в `docs/images/`**:
```
git diff origin/dev...HEAD --stat -- docs/images/
docs/images/screenshots.json | 44 ++++++++++++++++++++++----------------------
1 file changed, 22 insertions(+), 22 deletions(-)
```
Проверено вручную (sha256 каждого файла `docs/images/*.png` против записи в
манифесте): фактический хэш каждого из 10 файлов совпадает со **старым**
(dev-овым) значением, а не с тем, что теперь в манифесте — то есть `capture.mjs`
действительно перерендерил кадры (иначе `sourceFingerprint` не совпал бы с
текущим `src/**`, а он совпадает — стейл-фингерпринт не выдаётся), но
получившиеся PNG никогда не были закоммичены (`git add` пропущен). Это ровно
тот класс дефекта, о котором предупреждает AGENTS.md («докс — реальный
блокер») и о котором явно сказано в брифе ревью (пропуск этого шага уже дважды
стоил `dev` красного job `docs`, #230/#234) — здесь это тот же паттерн, только
внутри одной задачи.
Независимо подтверждено логом CI на HEAD (job `Предполётные проверки`, прогон
33319669425): тот же список из 10 `ERROR screenshot … image hash does not
match manifest`, `##[error]Process completed with exit code 1`.
Чинится в скоупе: перезапустить `npm run build && node demo/docs/capture.mjs`
на актуальном HEAD и на этот раз закоммитить и манифест, и все 10
регенерированных PNG (`git add docs/images/*.png docs/images/screenshots.json`),
затем `node scripts/check-docs.mjs --external` — обязательное условие зелёного
раунда.
### Low (снято с записью) — AC1 (coverage-baseline) недоказан прогоном ветки, но блокер внешний и уже заведён
`scripts/backend-coverage-baseline.txt` остаётся плейсхолдером `80.0` — тем же,
что автор дважды пометил как временный («после зелёного CI закоммичу точное
значение до вердикта»). Зелёного `backend`-job на этой ветке не было ни разу.
Причина — НЕ код этой задачи: `pytest` в CI job `backend` (HEAD, прогон
99276949757) валится 86 тестами с одинаковой ошибкой
`Setup failed for custom integration 'houseplan': No setup or config entry
setup function defined` — и **на самом `dev`** (прогон 33317335067, job
99273097390) те же 86 тестов падают с тем же сообщением, тем же числом. Уже
заведено отдельно: **issue #389** «Backend-тесты HA-харнесса красные на dev с
30.08». Это не регрессия #42, а внешняя блокировка.
Важная деталь для следующего раунда: pytest всё равно ДОХОДИТ до конца и пишет
coverage-отчёт несмотря на 86 упавших тестов — я вижу в логе job'а `TOTAL …
68%` — но шаг «Порог покрытия не ниже baseline» после него **не запускается**,
потому что предыдущий шаг (`pytest`) вернул ненулевой код и GitHub Actions
пропускает последующие шаги без `if: always()`. Число 68% — не «настоящий»
бейзлайн: интеграция не поднимается в 86 тестах, огромные куски кода вообще не
исполняются, так что цифра занижена тем же #389 и использовать её как реальный
бейзлайн нельзя.
Не блокирую этим раундом: причина полностью внешняя и уже отслеживается в
#389, а плейсхолдер не ухудшает состояние `dev` (там `backend`-job и так
красный по той же причине). Но снимаю не молча: как только #389 закроется и
`backend`-job этой ветки дойдёт до сравнения с baseline, число должно быть
снято заново и закоммичено настоящим — заглушка не должна доехать до бета-гейта.
### Low (снято с записью) — ruff-долг `scripts/`+`tests_backend/` шире объявленного `include`, но вне CI-скоупа
`pyproject.toml` объявляет `include` на `custom_components/houseplan/**/*.py`,
`scripts/*.py` и `tests_backend/**/*.py`, но CI (`validate.yml:782`) реально
линтит только `custom_components/houseplan`. При прогоне по полному `include`
(`python -m ruff check custom_components/houseplan scripts tests_backend`) —
53 находки; на самом `dev` таких же находок 52 (проверено на чистом чекауте
`dev`), то есть эта ветка добавила ровно одну новую — `I001` (порядок
импортов) в новом файле `tests_backend/test_backend_quality.py:5`. ТЗ явно
сузило блок 2 до `custom_components/houseplan` (щадящий трек, «без массового
rewrite»), так что это не нарушение AC. Не блокирую, но `include` в
`pyproject.toml` шире, чем то, что реально проверяется — вводит в заблуждение
при чтении конфига; можно поправить `include` или добавить одну сортировку
импорта в новом файле при следующей правке.
### Low (снято с записью) — golden-расхождений на самом деле 8, не 4, но все вне скоупа #42
Автор в первом фикс-комментарии назвал 4 расходящиеся golden-сцены
(`device-dialog-mobile-ru`, `device-dialog-desktop-de`,
`toggle-entity-dialog-mobile-ru`, `device-ripple-color-popover-mobile-ru`).
Лог job `Golden-кадры` на прогоне того же периода (33318774716) показывает
**8**: те же четыре плюс `device-inbox-desktop-en-light`,
`device-inbox-desktop-ru-dark`, `device-inbox-narrow-ru-dark`,
`tray-medium-group-en`. Диф #42 не касается ни иконок устройств, ни inbox/tray
— все 8 объясняются тем же посторонним коммитом `2e30daa3` (#74, иконки
`mdi:motion-play-outline`/`mdi:repeat`/`mdi:stop-circle-outline`), только
затрагивает больше сцен, чем автор перечислил. Не в скоупе этой задачи (golden
— зона #74/пре-релизный гейт), фиксирую как неточность подсчёта, не как
находку против #42.
## Что проверено и корректно
- **H1/H2 из r1** — закрыты по существу (см. таблицу закрытия раунда выше).
- **AC3 (ruff, CI-скоуп)** — чисто.
- **AC4 (mypy strict, 6 модулей)** — зелёный, воспроизведено `--config-file
pyproject.toml` (strict действительно применяется, не default-профиль).
- **AC5 (сканер ERROR_CODES/ERROR_CODE_FAMILIES, оба пути эмиссии)** — тест
проходит; мутант «убрать флагманский код из ERROR_CODES» воспроизведён
вручную и красный, как и требуется методологией ревью («тест умеет
падать»). i18n-паритет 22/22/22/22.
- **AC6 (фронт: JSON-details + legacy regex fallback + code-first)** —
`test/open-passage-contract.test.mjs` содержит прямой тест на `_errText`,
зелёный; диф `houseplan-card.ts` читаемо реализует именно это поведение
(JSON.parse → legacy regex → console.warn для сырого message → `err.code`
fallback).
- **B023-рефакторинг в `wall_segment_model.py`/`junction_limits.py`** —
прочитан построчно: замыкания вызываются исключительно внутри той же
итерации цикла, где определены, поведенческой разницы биндинг по умолчанию
не создаёт; подтверждено прогоном backend CI (строка покрытия модуля, тесты
этого файла не в списке 86 упавших).
- **Трейлеры и changelog** — `feat`-коммит `cec11a95` несёт `User-Visible: yes`
и правки обоих `docs/CHANGELOG*.md` в этом же коммите; формулировки совпадают
с фактическим изменением поведения.
- **quality_scale.yaml** — `docs-troubleshooting`/`docs-examples` → `done`
корректно (USER-GUIDE.ru §22 «Диагностика» действительно существует, что я
проверил grep'ом, а не поверил инвентаризации автора).
- Импорт `CONF_ADMIN_ONLY`, убранный из `websocket_api.py` при isort-правке —
проверено: нигде в файле не используется (используется только в `auth.py`,
где импорт остался) — не регрессия.
## Чего не проверял
- **Полный `pytest tests_backend/` с реальным HA-harness** — недоступен в
песочнице ревьюера (`ModuleNotFoundError: No module named 'homeassistant'`,
как и предупреждает AGENTS.md); опирался на лог CI-прогона этой же ветки на
точном HEAD, полученный и прочитанный независимо (не только со слов автора).
- **`npm run invariants`** — не гонял отдельно: диф не меняет геометрию
фронтенда (`src/**` геометрический код не тронут), а Python-геометрия
(`wall_segment_model.py`) — только рефакторинг замыканий без изменения
формул; `npm test` уже прогнал JS model-invariants на всех моделях проекта
и прошёл.
- **Браузерные смоки `demo/smoke_*.mjs`** — не гонял. `smoke-select.mjs` дал
«НЕОПРЕДЕЛЁННОСТЬ» на единственной изменённой строке `src/**`
(`_serverCfg` — широкий символ), а сама строка — только форматирование
текста ошибки, покрытое юнитом AC6; видимого рендер-пути диф не касается.
- **`golden:verify` вручную** — не гонял; диф не может изменить рендер (только
backend + текст ошибок), расхождения в CI golden проверены по логу и
атрибутированы постороннему коммиту #74 (см. Low выше), не пересчитывал
дифф картинок сам.
- **`python -m pytest tests_backend -q` целиком с покрытием** — не запускал
локально (нет HA); совпадение цифр (366 pass/86 fail, `TOTAL … 68%`) взято из
лога CI, не пересчитано мной построчно за пределами точечных грепов.
## Вердикт
Красный: High-1 (сломанный `docs`-гейт, самопричинён этой веткой, в скоупе,
воспроизведён и локально, и по логу CI) блокирует. H1/H2 из r1 закрыты
корректно. AC1 (baseline) не в счёт вердикта — блокирован внешним, уже
заведённым #389, и не регрессия этой ветки, но остаётся открытым пунктом до
следующего раунда.