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

99 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-42-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/42
- ТЗ: docs/specs/042-backend-engineering-quality.md, ревизия 6 (принята арбитражем владельца после исчерпания бюджета SPEC-REVIEW, 4/4)
- Диапазон материала: `origin/dev..HEAD`, HEAD = `ab337193abb8c629322313a0bf2d747105942db4`
- Коммиты: `313dfd07` (feat, User-Visible: yes) · `ffe8c3d1` (test) · `cfa8b5af` (test) · `ab337193` (build: bundle sync)
- Заход код-ревью: **r1**, блокирующих циклов израсходовано **0 из 4** до этого вердикта
- Трейлеры: у всех 4 коммитов корректны (`Issue: #42`, `User-Visible: yes|no`); `User-Visible: yes` на `313dfd07` сопровождён правками обоих changelog в том же коммите — выполнено.
## Скоуп
Полный трек (не `small`), диапазон затрагивает класс A (`custom_components/houseplan/**/*.py`, `src/houseplan-card.ts`, `src/i18n/*.json`), класс B (`.github/workflows/validate.yml`, `.github/workflows/mutation-gate.yml`, `scripts/mutation-gate.mjs`, `test/**`, `tests_backend/**`, `pyproject.toml`, `requirements_test.txt`), класс C (`docs/**`) и класс D (`dist/**`, `custom_components/houseplan/frontend/**`, коммит `ab337193` — bundle-sync после `feat`, по правилу класса D корректен). Первый код-ревью раунда — разбор полный по всем пяти блокам ТЗ, дельта по спецификации (§2.10) здесь неприменима: это код-ревью, а не повторный спек-раунд.
## Как проверялось
Зелёного Validate на HEAD (`ab337193`) не существует — оба прогона CI на этой ветке (`33316584357`, `33316653216`) красные на **фронтенд-** и **бэкенд-**джобах. Прогнал дешёвые гейты сам и независимо воспроизвёл обе причины красноты локально (см. таблицу и находки High).
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | чисто |
| Frontend unit | `npm test` | **1636/0**, 1 skipped — совпадает с хендоффом |
| Build + bundle sync | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` | совпадает байт-в-байт |
| Bundle budget | `npm run bundle:budget` | 282 738 / 300 000 B gzip — совпадает с хендоффом |
| no-new-any | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | **КРАСНЫЙ** — см. Finding H1 |
| check-docs (src/** тронут) | `node scripts/check-docs.mjs` | чисто (7 файлов, 10 внешних ссылок) |
| i18n паритет | скрипт сверки ключей en/ru/de/fr | 22/22/22/22 добавленных ключа идентичны по имени во всех 4 словарях |
| Backend install (как в CI) | `pip install -r requirements_test.txt` в чистом venv | **КРАСНЫЙ** — воспроизведено независимо от CI, см. Finding H2 |
| Backend pure pytest (изолированный venv без застрявшего requirements_test.txt) | `pytest tests_backend/test_backend_quality.py -q` | 3/3 пройдено |
| — то же, `test_validation.py` | `pytest tests_backend/test_validation.py -q` | 142 passed, 1 skipped |
| ruff (узкий набор, установлен вручную вне сломанного requirements_test.txt) | `ruff check custom_components/houseplan --config pyproject.toml` | «All checks passed!» |
| mypy strict (6 модулей allowlist) | `mypy -p const -p projection -p coordinate_canonicalization -p frontend_asset_manifest -p junction_limits -p plans` | «Success: no issues found in 6 source files» |
| Мутанты AC5 (ручная проверка m1c) | удалил `invalid_light_entity` из `ERROR_CODES`, перезапустил `test_backend_quality.py` | тест **упал** ожидаемо, затем восстановлено (`git status` чист) |
| smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | «НЕОПРЕДЕЛЁННОСТЬ»: диф трогает 1 файл `src/**`, единственный широкий символ `_serverCfg` — ни один смок не привязан доказуемо. Решение: не гонять смоки — AC6 покрыт юнитом, поведение `_errText` не имеет отдельного визуального следа |
| golden / performance / полный HA-harness | — | не гонялись: не изменяют визуал/перф; предрелizный гейт (§8) |
| `python -m pytest tests_backend -q` (полный, с HA) | — | невозможно в песочнице — `homeassistant` не установлен, `.venv-backend` отсутствует (см. AGENTS.md «Backend»); эквивалент — независимая проверка через изолированный venv + чтение кода |
## Находки
### High H1 — `no-new-any` красный на HEAD: новый необоснованный `any`
`src/houseplan-card.ts:9743` (добавлено `313dfd07`):
```ts
const space = this._serverCfg?.spaces?.find((item: any) => String(item.id) === spaceId);
```
`_serverCfg` типизирован (`ServerConfig`), и везде рядом `spaces` используется без нового `any` (`this._serverCfg?.spaces || []`, `.spaces.length` и т.д. — уже типизированный массив). Аннотация `: any` здесь не обоснована ни отсутствием типа, ни комментарием `// any-ok: …`. Гейт `scripts/no-new-any.mjs` (обязательная часть локального набора, §8) красный и на CI (job «Фронтенд: типы, юниты, мутанты, синхрон бандла», оба прогона ветки), и локально — воспроизведено дословно тем же выводом.
Чинится в скоупе: убрать `: any` (тип элемента `spaces` выводится сам) либо использовать точный тип элемента.
### High H2 — `requirements_test.txt` невозможно установить: бэкенд-CI не работал ни разу
`requirements_test.txt` пинует одновременно:
```
pytest==9.1.1
pytest-homeassistant-custom-component==0.13.45
pytest-cov==7.1.0
```
но `pytest-homeassistant-custom-component==0.13.45` сам жёстко требует `pytest==7.3.1` и `pytest-cov==3.0.0` (проверено чтением METADATA пакета). Это неразрешимый конфликт версий, а не транзиентная сетевая ошибка:
- воспроизведено в CI дважды подряд, на обоих прогонах ветки (`30f45e53`, `ab337193`): `pip install -r requirements_test.txt` → `ResolutionImpossible`;
- воспроизведено независимо в чистом sandbox-venv (не CI-окружение) той же командой — идентичная ошибка.
Это ровно Блок 1 ТЗ («requirements_test.txt — единая точка правды зависимостей»), и он ломает **весь** бэкенд-джоб CI: до `pytest` дело не доходит вообще — ни ruff-шаг, ни guard тихого скипа harness, ни сам pytest с `--cov`, ни сравнение с baseline. Следствия:
- **AC1** (coverage.xml + сравнение с baseline) не доказан прогоном ветки, как того явно требует сама формулировка AC1 — прогона просто не было ни разу;
- **AC2** (защита от тихого скипа harness) не доказан по той же причине;
- `scripts/backend-coverage-baseline.txt` остаётся плейсхолдером **`80.0`**, который автор в хендоффе прямо назвал заглушкой и обещал заменить «после зелёного CI» — зелёного CI не случилось, обещание не выполнено, а нормативный текст ТЗ (раздел «Принятые предположения») требует закоммитить фактическое число до код-ревью;
- заявление хендоффа «pytest pure 243/0» не могло быть получено прогоном именно этого `requirements_test.txt` (независимо перепроверено: сборка зависимостей по этому файлу невозможна в принципе, а не «медленная» или «требует HA»); откуда взято число — не сказано, и с той же командой из ТЗ оно не воспроизводится;
- зеркало в `.github/workflows/mutation-gate.yml:67` ставит зависимости той же командой — мутационный гейт по Python-гвардам (`error-code-dropped-from-contract`, `error-scanner-loses-a-class-source`, `error-code-via-variable-dropped`) в CI тоже не может выполниться.
Логика самих CI-шагов (сравнение с baseline, порог `≥50` для `test_ha_*`, upload артефакта) прочитана и выглядит корректной — проблема ровно в пиннинге зависимостей, не в механизме.
Чинится в скоупе: привести `requirements_test.txt` к разрешимому набору (например, снять собственный пин `pytest-cov`/`pytest` до версий, совместимых с `pytest-homeassistant-custom-component==0.13.45`, либо поднять `pytest-homeassistant-custom-component` до версии, допускающей `pytest-cov==7.1.0` — выбор версии `оставлен` автору), прогнать реальный CI на итоговом коммите и закоммитить фактический baseline до повторного ревью.
## Проверено и корректно
- **Блок 5 (WS error contract), логика сканера** — прочитан построчно и проверен исполнением в изолированном venv: `ERROR_CODES`/`ERROR_CODE_FAMILIES` (`const.py`) и сканер (`tests_backend/test_backend_quality.py`) корректно находят оба пути эмиссии — литералы `send_error`, четыре validation-класса с `code = "..."`, `JunctionLimitError` (f-string семейство), `MarkerControlError` литералами И через переменные (`source_error`/`attribute_error`, кортеж `code`). Мутант m1c (удаление `invalid_light_entity` из `ERROR_CODES`) вручную воспроизведён — тест падает с понятной ошибкой, затем код восстановлен и `git status` чист.
- **i18n**: 22 новых ключа `backup.error.*` добавлены идентично и полно во всех 4 словарях (en/ru/de/fr) — сверено скриптом, не только заявлением автора.
- **`_errText` code-first (AC6, M2 r1)**: порядок проверок в `houseplan-card.ts:9765` — код проверяется раньше `e.message`; JSON.parse первым, legacy regex — read-compat fallback с deprecated-комментарием и датой; неизвестный код рендерит `err.code`/`err.unknown`, а не сырой `e.message` (уходит в `console.warn`). Прочитано построчно, поведение соответствует тексту ТЗ блока 5.
- **mypy strict allowlist (AC4)**: прогнан исполнением на всех 6 модулях — чисто. `junction_limits.py` (геометрия!) проверен построчно на предмет поведенческих изменений: диф — исключительно добавление type hints (`dict[str, Any]`, `list[float]` и т.п.), логика функций не тронута ни в одной строке — инварианты модели (`npm run model-invariants`) не требуются, т.к. геометрическое поведение не менялось (проверено чтением, не исполнением).
- **ruff narrow (AC3)**: прогнан исполнением — чисто; noqa-контракт-тест (`test_issue_42_every_noqa_carries_a_reason`) проверен — все новые `noqa: BLE001` в `websocket_api.py`/`http_api.py` несут причину ≥10 символов.
- **Structured JSON details**: `OpeningPassageError`/`PartitionOpeningJambMarginError` теперь сериализуют `json.dumps(...)`; `test_validation.py` обновлён на `json.loads(...)` и по-прежнему проверяет отсутствие утечки `binary_sensor` в сообщении; прогнан исполнением — зелёный.
- **quality_scale.yaml**: `docs-troubleshooting`/`docs-examples` → `done` — проверено, что `docs/USER-GUIDE.ru.md` уже содержит «## 22. Диагностика» **до** начала работы над #42 (введено в v1.59.0-rc.2, задолго до этого issue) — заявление хендоффа о ложном пробеле («инвентаризация грепала английское слово») подтверждено; никакой недостающей работы по документации не скрыто.
- **Changelog×2, ARCHITECTURE.md**: правки внесены в том же коммите `313dfd07`, что и поведение; описывают ровно видимое изменение (локализованные тексты ошибок).
- **Одно число — один источник**: этот дифф не вводит новую пользователем видимую величину, дублируемую в двух местах. `margin_cm`/`fields` — единственное место рендера (`_errText`), источник один (JSON от бэкенда), изменился только формат парсинга (JSON вместо regex), а не число мест вычисления. `test/single-source-numbers.test.mjs` не входит в затронутый диапазон и не требовал перепрогона.
- **Коммиты класса B** (`ffe8c3d1`, `cfa8b5af`) корректно переиспользуют `Issue: #42`, `User-Visible: no`.
## Чего не проверял и почему
- **Полный `pytest tests_backend/ -q` с реальным HA-harness** — недоступен в песочнице (`homeassistant` не установлен, `.venv-backend` отсутствует); по AGENTS.md это ожидаемо для этого окружения. Компенсировано изолированным venv для «чистых» файлов (`test_backend_quality.py`, `test_validation.py`) и чтением остального с исполнением мутанта.
- **Полный код `websocket_api.py` (925 строк) построчно** — диф в этом файле (168 строк) целиком мехнический (реордер импортов + noqa-причины); прочитан полностью, поведенческих изменений вне диффа не искал построчно по всему файлу — вне скоупа диффа.
- **Браузерные смоки** — не гонялись; `smoke-select.mjs` вернул «НЕОПРЕДЕЛЁННОСТЬ» (единственный тронутый `src`-файл, единственный широкий символ `_serverCfg`), в АС ни один смок не назван. Решение снять — риск узкий (текстовый рендер ошибки), покрыт юнитом AC6.
- **golden/performance/полный HA-harness** — предрелизные гейты (§8, §11.4), не гейт этого ревью; диф не меняет визуал/геометрию рендера.
- **`import_export.py`, `http_api.py`, `__init__.py`, `frontend_assets.py`, `trails.py`** — прочитаны целиком по своим (коротким, 1–22 строк) диффам: везде реордер импортов / удаление неиспользуемых (`F401`) / добавление причины к `noqa`, поведенческих изменений не обнаружено.
## Вердикт
**Красный.** Оба High-находки — не стилистические придирки: `no-new-any` красный на HEAD и `requirements_test.txt` невозможно установить — это ровно те «дешёвые гейты», которые обязаны быть зелёными перед выходом из «В разработке» (§8), и они не были зелёными ни разу за оба прогона CI этой ветки. Раз ревью кода отвечает на вопрос «оно вообще работает» вместо ручного тестирования — ответ по бэкенд-блоку (Блок 1/4, AC1/AC2) сейчас «не проверено ни разу», а не «работает». Оба фикса локальны и в скоупе задачи (одна аннотация типа; пересборка одного файла зависимостей), пятого повторного цикла не требуют технически, но по букве процесса должны пройти повторный код-ревью после исправления (§4).