# CODE-REVIEW-42-r6 - Issue: #42 — «Измеряемое инженерное качество backend» - Этап: code (PROCESS.md §2.7) - Заход: r6 · блокирующих циклов израсходовано (до этого вердикта) 3 из 4 - SHA: `972e708a71d7a67e7abf2ccd7d7e3876c6cc3c50` - Вердикт: **жёлтый** · High: 0 · Medium: 1 (в скоупе) ## Скоуп раунда и почему разбор полный, а не по дельте r5 (SHA `718c20c4`) был зелёным (High 0/Medium 0). Публикация документа r5 трижды падала не на коде задачи, а на собственном гейте процесса (лимит документов ревью считал SPEC-REVIEW и CODE-REVIEW в одну корзину — заведено и исправлено как #395, `066cf44c`, уже в `dev`). Пока задача ждала фикса, ветку пришлось ещё раз ребейзнуть на ушедший вперёд `dev` (принёс сам фикс #395 плюс докс-коммиты #391) — HEAD стал `972e708a`. Ребейз на ушедший вперёд `dev` — по правилу этого процесса (§7.2/§2.10) означает другой код, поэтому разбор в этом раунде **полный**, по всем AC1– AC7, а не только по тому, что задело слияние. Заново прогнаны все дешёвые гейты и вручную воспроизведены все зарегистрированные мутанты — не только принято на слово из текста r5. Независимо подтверждено (не только текстом issue): `git merge-base origin/dev HEAD` = `066cf44c` (текущий tip `dev`), `git log --oneline origin/dev..HEAD` даёт ровно 16 «родных» коммитов #42, ни одного чужого. Диф собственного кода #42 (`src/**`, `custom_components/**/*.py`, `tests_backend/**`, `test/**`) относительно `dev` изучен построчно заново (см. ниже) — по содержанию совпадает с тем, что описывал r5 (только рефренс-SHA сменился из-за ребейза). ## Закрытие раунда r5 r5 был зелёным — находок для закрытия нет. Таблица ниже фиксирует не «закрытие», а то, что было перепроверено заново в этом раунде, а не унаследовано вслепую: | Из какого раунда | Что перепроверено в r6 | Чем подтверждено | |---|---|---| | r4→r5, M1 (сканер AC5 не парсил кортеж структурно) | Структурный разбор `(field, code, message)` в `test_backend_quality.py` | Лично внедрил третий элемент кортежа (`invalid_ghost_entity_mutant_probe`) в `validation.py:905-916` → тест красный; откат → зелёный | | r4→r5, M1c (код через переменную) | `invalid_light_entity` доказан поимённо | Лично удалил код из `const.ERROR_CODES` → тест красный; откат → зелёный | | r4→r5, M2 (реюз-ключ backend не включал новые файлы) | `scripts/gate-reuse.mjs` roots содержат `tests_backend`, `pyproject.toml`, `scripts/backend-coverage-baseline.txt` | Прочитал текущий `HARNESS.backend.roots` — все три входа на месте; `node --test test/gate-reuse.test.mjs` → 13/13 | | r3→r5 (mypy strict allowlist) | 6/6 модулей действительно проходят `mypy --strict` | Прогнал `mypy --config-file pyproject.toml` в изолированном venv → «Success: no issues found in 6 source files» | | r1→r5 (fail-closed AC5-сканер для непроверяемого источника кода) | Ветка «MarkerControlError с непрослеживаемым аргументом» | Лично внедрил `MarkerControlError(untracked_mutant_var, ...)` → сканер упал с явным сообщением «cannot prove»; откат | ## Унаследовано из r5 (без личного перезапуска тяжёлых job) | Что | Документ/SHA-источник | Почему можно не гонять заново | |---|---|---| | Полный HA-harness `pytest tests_backend/` (453 passed/2 skipped, coverage 87.2%==baseline) | CODE-REVIEW-42-r5, живой Validate run `33326532290` на SHA `718c20c4` | Независимо перепроверил через `gh run view` — job «Бэкенд: pytest в Home Assistant» = success, лог содержит точно `coverage: 87.2% (baseline 87.2%)`, `453 passed, 2 skipped`, `collected HA-harness tests: 203`. Вход job (`tests_backend/**`, `custom_components/**/*.py`, `pyproject.toml`, baseline-файл) не менялся между `718c20c4` и `972e708a` — сверено построчным дифом каждого файла в этом документе ниже | | `golden:verify`, 3 смок-шарда, perf-smoke | Тот же run `33326532290` | Все 4 job = success на этом же прогоне; `src/**` не менялся с полностью смок-протестированных r3/r4 (единственный диф в `houseplan-card.ts` — тот же, что был на r1-r5, `smoke-select` подтверждает «1 файл, 1 символ») | | USER-GUIDE.ru §22 «Диагностика», docs-examples YAML | r3/r5 | `git diff origin/dev...HEAD -- docs/USER-GUIDE*.md` = пусто в этом раунде — контент не менялся вообще, значит и вывод не может измениться | | Живой Validate-прогон на текущем SHA `972e708a` | — | Run `33328530255`: reuse-job «Переиспользование: это дерево уже проверено» = success, поэтому backend/golden/3-смока/perf идут `skipped` — реюз, не пропуск. Проверил основание сам: входы реюз-ключа backend (`tests_backend`, `custom_components/**/*.py`, `pytest.ini`, `pyproject.toml`, baseline-файл) байт-в-байт совпадают с деревом `718c20c4`, потому что единственные коммиты между ними — докс/бандл (см. дифф-статы ниже) | ## Как проверялось (полный прогон в этом раунде) Прогнал сам (зелёного Validate на `972e708a` нет — единственный завершённый прогон `33328530255` красный на «Предполётных проверках», см. находку про `process-gate` ниже; тяжёлые job на нём `skipped` по легитимному реюзу, проверено отдельно): - `npx tsc --noEmit` — чисто. - `npm test` — **1647 passed / 0 failed / 1 skipped**. - `npm run bundle:sync` (= `build` + `bundle-sync.mjs`) — `git status` после прогона пуст, диска не тронул (никакого diff в `dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets`). - `node scripts/bundle-budget.mjs` — 284047/300000 (запас 15953 Б). - `node scripts/no-new-any.mjs --base origin/dev --head HEAD` — «Новых any нет» (36 добавленных строк в 1 файле). - `node scripts/check-docs.mjs --external` — «Documentation checks passed (7 files, 10 external links)». - Изолированный venv (`python3 -m venv`, сеть в песочнице доступна): - `ruff==0.16.5 check custom_components/houseplan` (ровно CI-скоуп из `validate.yml:788`) — «All checks passed!». - `mypy --config-file pyproject.toml` на всех 6 модулей allowlist — «Success: no issues found in 6 source files». - Pure-pytest (`tests_backend`, кроме `test_ha_websocket.py` и `test_coordinate_canonicalization.py` — тянут `homeassistant`, которого в песочнице нет) — **244 passed, 1 skipped**. - `tests_backend/test_backend_quality.py` отдельно — 3/3. - Мутанты (все 5, включая незарегистрированную в mutation-gate.mjs fail-closed ветку) воспроизведены руками правкой файла, прогоном теста, откатом; `git status` после каждого — чист: - м1 (`invalid_space_id` удалён из `ERROR_CODES`) → красный. - м1b (регэксп class-attr в сканере затихшен) → красный. - м1c (`invalid_light_entity`, код через переменную, удалён из `ERROR_CODES`) → красный. - структурный тест кортежа (инъекция `invalid_ghost_entity_mutant_probe` третьим элементом) → красный. - fail-closed для непрослеживаемого аргумента `MarkerControlError` → красный с явным сообщением сканера. - м2 (JSON-ветка `_errText` вырезана) → `node --test test/open-passage-contract.test.mjs` красный (1 fail из 8). - `node --experimental-test-coverage --test test/gate-reuse.test.mjs` — 13/13. - i18n-паритет: `en/ru/de/fr` — по 1189 ключей каждый, множества ключей идентичны (сверено программно, не только count). - `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — **НЕОПРЕДЕЛЁННОСТЬ** (1 файл `src/**`, широкий символ `_serverCfg`, порог 41). Браузерные смоки и `golden:verify` лично не гонял — см. таблицу «Унаследовано» выше (тот же код уже зелёный в живом CI на `718c20c4`, `src/**` не менялся с той проверки). - `EVENT_NAME=push BEFORE_SHA=$(git rev-parse origin/dev) BASE_SHA=$(git rev-parse origin/dev) HEAD_SHA=$(git rev-parse HEAD) DEVELOPMENT_BRANCH=dev node scripts/process-gate.mjs --github-range --issues` — «гейт пройден, предупреждений 1» (см. WARN-находку ниже). - Прочитал построчно (не исполнял — не геометрия/поведение): `custom_components/houseplan/wall_segment_model.py`, `custom_components/houseplan/junction_limits.py` — диф только type hints и связывание loop-переменных параметрами по умолчанию (B023-рефакторинг); одна удалённая мёртвая переменная `zero_segments` (F841) — подтвердил `grep`, что она нигде больше не читается. Behaviour-preserving; `npm test` уже гоняет инварианты модели на фикстурах проекта и прошёл. `npm run invariants` отдельно не гонял — геометрия по чтению не менялась. - Trailers: все 16 коммитов несут `Issue: #42`; ровно один (`323b7803`) — `User-Visible: yes`, и он же в одном коммите правит оба `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md`. ## Находки ### Medium (в скоупе, чинится в этой же ветке) **Mypy strict (блок 3, AC4) не имеет механизма исполнения в CI — только текстовый контракт-тест, который его не проверяет.** Спецификация (rev6, блок 3) вводит `mypy strict` как один из пяти инженерных гейтов issue наравне с coverage и ruff. Реализация: `pyproject.toml` содержит `[[tool.mypy.overrides]]` с allowlist из 6 модулей и `strict = true`; `tests_backend/test_backend_quality.py:: test_issue_42_mypy_strict_allowlist_only_grows` — но этот тест **не запускает mypy вообще**: он текстовым regex'ом сравнивает committed-список модулей с зафиксированным множеством и падает только при **удалении** модуля из списка. Он не может обнаружить, что код внутри allowlist-модуля перестал проходить strict-проверку. Ни в одном workflow (`validate.yml`, `mutation-gate.yml` — проверил оба, и вообще `grep -rl mypy .github/workflows/` пуст) нет шага, который бы запускал `mypy`. `mypy` также не пиновится в `tests_backend/requirements.txt` — файл содержит `ruff==0.16.5` с явным комментарием «#42: линт бэкенда... ставится отсюда же», но эквивалентной строки для mypy нет. То есть CI физически не может прогнать mypy сегодня — не установлен. Итог: «mypy strict зелёный на стартовом allowlist» проверяется только вручную ревьюером в момент код-ревью (я лично прогнал — 6/6 чисто, см. выше), а не автоматически. Достаточно любого следующего PR, трогающего `const.py`/`projection.py`/`plans.py`/… без явного намерения нарушить типизацию, и strict-регрессия войдёт в `dev` полностью незамеченной: CI останется зелёным. Это прямо противоречит цели issue («**измеряемое** инженерное качество») — для coverage и ruff issue построил именно такой continuous-гейт (шаги «Линт бэкенда» и «Порог покрытия» в `validate.yml:787-816`), а для typing — нет, хотя структурно это тот же самый паттерн и добавляется тем же способом. **Формально ли это нарушение AC4?** Нет — AC4 сама помечена в тексте ТЗ как «(локально)» (в отличие от AC1/AC2 — «(CI)»), и в этом узком смысле код соответствует букве. Но эта деталь формулировки ТЗ ни разу не поднималась и не обсуждалась ни в одном из 4 раундов SPEC-REVIEW (все они были посвящены исключительно блоку 5 — контракту кодов ошибок) — она не результат осознанного арбитража, а незамеченный побочный эффект формулировки. По существу задача решает сценарий «типизация может тихо деградировать без сигнала» ХУЖЕ, чем декларирует собственный заголовок issue, и это ровно тот класс вопроса («изменение не решает заявленный сценарий»), который жёлтый вердикт вправе поднимать при формально выполненных AC. **Как чинится в скоупе** (маленькая, локальная правка, по образцу уже существующего ruff-шага): 1. добавить `mypy` с пином в `tests_backend/requirements.txt` (комментарий по аналогии со строкой ruff); 2. добавить шаг в `validate.yml` после «Линт бэкенда»: `python -m mypy --config-file pyproject.toml -p custom_components.houseplan.const -p custom_components.houseplan.projection -p custom_components.houseplan.coordinate_canonicalization -p custom_components.houseplan.frontend_asset_manifest -p custom_components.houseplan.junction_limits -p custom_components.houseplan.plans` (список модулей уже есть в `pyproject.toml` — можно даже читать его оттуда скриптом, но это опционально); 3. обновить `scripts/gate-reuse.mjs` не требуется — `pyproject.toml` уже входит в roots backend job (M2 r4). Технический вопрос (как именно провести mypy через CI), не продуктовый — не выносится владельцу, решение предложено выше. ### Low (снято с записью, не блокирует) 1. **`pyproject.toml` `[tool.ruff] include`** перечисляет `scripts/*.py` и `tests_backend/**/*.py` наравне с `custom_components/houseplan/**/*.py`, но CI линтит только `custom_components/houseplan` (`validate.yml:788`, соответствует явному тексту блока 2 спецификации). Если запустить `ruff check` по всему `include`-набору — 56 находок (E402×22, I001×12, B023×7 — в том числе в `tests_backend/test_validation.py:449-456`, тот же класс, что чинился в этом issue, но не исследованный, — B017×5, F401×3, E702×3, F811×2, E401×2). Все они в коде, который #42 не трогал (проверено: `git diff origin/dev...HEAD -- tests_backend/test_validation.py` меняет только одну JSON-ассерцию, строки 449-456 не задеты) и вне объявленного спецификацией CI-скоупа. Конфиг вводящий в заблуждение (создаёт впечатление более широкого линта, чем реально исполняется), но не является регрессией этого issue и не ломает ни один AC — оставляю без действия в рамках #42, можно поправить `include` отдельной мелкой задачей при следующем контакте с `pyproject.toml`. 2. **`process-gate` красный на живом CI SHA `972e708a`** (run `33328530255`, шаг «Процессный гейт»): `FAIL п.8 статус issue - issue #395: статус не проставлен`. Тот же класс гонки `BEFORE_SHA`, что уже документирован и признан не дефектом #42 в CODE-REVIEW-42-r3 и -r5 (в этот раз попутный коммит — `066cf44c` вместо `#392`/`#394`). Независимо подтверждено: пересчёт с корректным `BASE_SHA` = текущий tip `dev` даёт диапазон в 16 «родных» коммитов #42 и гейт **проходит** (см. «Как проверялось» выше). Не блокирует; не заводил отдельный issue — класс дефекта уже задокументирован в `scripts/process-gate.mjs` по трём предыдущим инцидентам, четвёртый инцидент того же класса не даёт нового знания. 3. **WARN п.0 классификации путей** (`custom_components/houseplan/ quality_scale.yaml`, `pyproject.toml` — вне классов A/B/C/D) — тот же пункт, что отмечен Low в r5, инструментальный пробел вне скоупа #42, не блокирует. ## Что проверено и корректно - AC1/AC2 (coverage-гейт CI, защита от тихого скипа harness) — доказаны живым прогоном ветки (`718c20c4`, независимо перечитан лог: 453 passed/2 skipped, coverage 87.2%==87.2%, collected 203 ≥ 50). - AC3 (ruff narrow, noqa с причиной) — чисто в объявленном CI-скоупе; каждый `noqa: BLE001` в диффе несёт причину ≥10 символов (проверил все найденные `noqa`-строки в `websocket_api.py`/`http_api.py` глазами). - AC4 (mypy strict, allowlist только растёт) — сам allowlist корректен и проходит strict (см. Medium выше про отсутствие continuous-проверки). - AC5 (сканер обоих путей эмиссии кодов) — 48 кодов + 3 семейства, структурный разбор кортежа, fail-closed для непрослеживаемых источников — все ветки лично воспроизведены как красные при поломке. - AC6 (JSON details + fallback + code-first) — `_errText` code-first, сырой английский текст уходит в `console.warn`, JSON.parse с legacy regex-фоллбэком; юнит-тест 8/8, мутант m2 воспроизведён красным. - AC7 — pure-pytest 244 (≥240 из ТЗ), бюджет бандла в пределах, i18n 1189×4 без расхождений. - Trailers/changelog — 16/16 коммитов с `Issue: #42`; единственный `User-Visible: yes` правит оба CHANGELOG в одном коммите. - Unused-import/dead-code чистка (F401×~6, F841×2) в `__init__.py`, `import_export.py`, `websocket_api.py`, `plans.py` — построчно проверил, что удалённые имена (`FILES_URL`, `PLANS_URL`, `CONF_ADMIN_ONLY`, `copy`, `old_refs`, `zero_segments`) больше нигде в своих модулях не используются — не регрессия. - `wall_segment_model.py`/`junction_limits.py` — B023-рефакторинг и type-hints, поведение не менялось (построчное чтение + `npm test` зелёный на фикстурах). ## Чего не проверял - Полный HA-harness `pytest tests_backend/` лично (нет `homeassistant` в песочнице) — опёрся на лог живого CI-прогона `718c20c4` (`33326532290`), независимо перечитанный, плюс подтверждённое совпадение входов реюз-ключа с текущим SHA. - Браузерные смоки (`demo/smoke_*.mjs`) и `golden:verify` лично не гонял — `smoke-select` дал НЕОПРЕДЕЛЁННОСТЬ, но `src/**` не менялся с уже полностью смок-протестированных r3/r4, и все job зелёные в живом CI на `718c20c4`; реюз-ключ на `972e708a` подтверждает те же входы. - Performance-профили — не названы в AC, диф не затрагивает чувствительные к перфу пути. - `npm run invariants -- --config <...>` отдельной командой — диф геометрических модулей ограничен type hints и B023-биндингом (поведенчески эквивалентен), `npm test` уже прогоняет инварианты на моделях проекта и прошёл. - Полный ruff по всему `pyproject.toml`-`include` (только CI-скоуп) — см. Low-1.