18 KiB
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):
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) — сверено скриптом, не только заявлением автора. _errTextcode-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).