# SPEC-REVIEW-42-r1 - Issue: #42 «[HP-ENG-01] измеряемое инженерное качество backend» - Этап: ТЗ на ревью (PROCESS.md §2.4), полный трек (метки: P2, tests, tech-debt, S4-spec-review; `small` отсутствует) - Артефакт ТЗ: `docs/specs/042-backend-engineering-quality.md`, ревизия 2 (коммит `582d673a`, docs-only, класс C) - Заход: r1 (документов `SPEC-REVIEW-42-*.md` в репозитории не найдено — первый заход подтверждён) - Ревьюер: свежая сессия, без контекста написания ТЗ ## Скоуп проверки ТЗ описывает одну ступень из пяти блоков: tooling-фундамент (pyproject + requirements_test), ruff narrow, mypy strict на растущем allowlist, coverage-механизм в CI с baseline-файлом, и формализация WS `ERROR_CODES` + JSON-details + локализованный fallback (единственная видимая пользователю часть). Дифф этого раунда — только сам файл ТЗ (177 добавлено / 58 удалено), продуктовый код не тронут. ## Как проверялось Ревью ТЗ на этом этапе — не код-ревью: гейты `typecheck`/`test`/`build` и браузерные смоки к докс-диффу неприменимы (класс C, ноль файлов `src/**` или `custom_components/**/*.py` в этом коммите). Вместо этого перепроверялись фактические утверждения ТЗ по текущему коду `dev` — то, что обычно и выдаёт догадку, поданную как факт: - `python3 -m ruff check custom_components/houseplan --select E,F,B,I --target-version py313 --statistics` → **333 (E501 291, B023 17, I001 13, F401 6, E731 2, F841 2, B905 2)** — совпадает с цифрами ТЗ дословно; - `custom_components/houseplan/quality_scale.yaml` → ровно 4 `todo`: `test-coverage`, `docs-troubleshooting`, `docs-examples`, `strict-typing` — совпадает с заявлением владельца в issue; - `docs/USER-GUIDE.md` §22 Troubleshooting существует (строка 1034), `docs/USER-GUIDE.ru.md` раздела «Troubleshooting»/«Устранение неполадок» не содержит вовсе — совпадает; - `grep -c '"backup.error\.' src/i18n/en.json` → **26** ключей — совпадает; - `.github/workflows/mutation-gate.yml:67` действительно дублирует строку `pip install pytest voluptuous pytest-homeassistant-custom-component home-assistant-frontend` из `validate.yml` — совпадает; - прочитан весь путь ошибки на фронте (`src/houseplan-card.ts:9497-9536` `_errText`, `src/houseplan-editor-runtime.ts:8002-8010` `_backupErrorText`) и все точки эмиссии `send_error` (`custom_components/houseplan/ websocket_api.py`, 54 вызова) плюс классы с публичным `.code` (`validation.py`, `junction_limits.py`, `import_export.py`) — см. находку Medium 1. Не запускался: `npm run typecheck/test/build`, `check-docs`, `model-invariants`, браузерные смоки, `pytest tests_backend`, mypy/coverage прогон — см. «Чего не проверял». ## Находки ### Medium 1 (в скоупе) — AC5 не покрывает коды, эмитируемые не литералом в `send_error(...)` **Файл:** `docs/specs/042-backend-engineering-quality.md`, раздел «5. WS error contract + доки» и «AC5». **Формулировка ТЗ:** «ERROR_CODES ⊇ все коды send_error (скан исходника): каждый литерал `send_error(...)`-кода ∈ ERROR_CODES». Как метод верификации это подразумевает статический скан аргументов вызовов `send_error(...)` на литеральные строки. **Почему это не выполнимо как написано.** Как минимум четыре класса ошибок несут код не литералом внутри `send_error(...)`, а через `err.code`, прочитанный в обработчике (`websocket_api.py:189` `send_error(msg_id, err.code, err.message)`; `:1370` и `:1747` `send_error(msg["id"], err.code, str(err))`): - `validation.py:43-54` `OpeningPassageError.code = "invalid_passage_fields"` и `validation.py:63-72` `PartitionOpeningJambMarginError.code = "invalid_partition_opening_jamb_margin"` — это ровно те два кода, вокруг которых построен весь блок 5 (JSON-details). Скан по литералам `send_error(...)` их не найдёт — они читаются из атрибута класса, а не передаются строкой в месте вызова; - `validation.py:57-60` `PartitionOpeningHostError.code = "invalid_partition_opening_host"` и `validation.py:81-84` `WallModelClientOutdatedError.code = "wall_model_client_outdated"` — то же; - `validation.py:35-40` `MarkerControlError.__init__(self, code, message)` — код передаётся аргументом конструктора; часть литеральна (`"duplicate_marker_control"`, `"marker_control_self"`, `"invalid_value_badge"` и др., `validation.py:824-968`), часть собрана f-строкой из `prefix` с ровно двумя значениями (`validation.py:818`: `"value_badge"` / `"value_source"`) → `value_badge_marker_missing`, `value_source_marker_missing`, `value_badge_marker_not_light`, `value_source_marker_not_light` (`validation.py:847,849`); - `junction_limits.py:56` `JunctionLimitError.code = f"junction_limit_{rule}"` — `rule` пробегает конечное множество ключей П1–П4 (`junction_limits.py:427-430`), но опять не литерал внутри `send_error(...)`. Итого не меньше дюжины уже существующих кодов — включая **оба** кода, ради которых написан блок 5 — невидимы для скана, читающего только литералы в `send_error(...)`. Реализация AC5 «в лоб» даст зелёный контракт-тест, который ничего не доказывает для этих кодов: они не попадут в `ERROR_CODES`, останутся без ключа `backup.error.`, и `_errText` продолжит показывать сырой `e.message` (`houseplan-card.ts:9528`) ровно для того класса ошибок, который issue называет проблемой. Это технический, не продуктовый вопрос (какой метод верификации использовать), поэтому решаю его в вердикте, а не выношу владельцу. **Как чинится в скоупе:** AC5/раздел 5 должны явно назвать способ, которым скан достаёт коды из `err.code` — либо (а) перечислить классы-источники (`OpeningPassageError`, `PartitionOpeningHostError`, `PartitionOpeningJambMarginError`, `WallModelClientOutdatedError`, `MarkerControlError`, `JunctionLimitError`, `ImportFailure`) и извлекать их `code`-литералы/шаблоны статическим разбором модуля, либо (б) явно сузить AC5 до кодов-литералов в `send_error(...)` и отдельно перечислить раскрытые f-строкой/классом семейства как «покрыты общим локализованным fallback по коду, без выделенного ключа» — тогда `_errText`/`_backupErrorText` обязаны фактически падать в этот fallback для них, а не в сырой `e.message` (что снова упирается в порядок проверок в `_errText`, см. Medium 2). ### Medium 2 (в скоупе) — раздел «i18n» (обязателен по PROCESS.md §7.1) отсутствует; повторное использование `err.code` не зафиксировано **Файл:** тот же, ТЗ целиком — раздела с заголовком «i18n» нет ни одного. DoR §2.5 требует «i18n: ключи en + ru перечислены». Блок 5 обещает: «неизвестный код → общий локализованный текст + код» — но не говорит, это НОВЫЙ ключ или переиспользование существующего. Проверка кода показывает, что подходящий ключ уже есть и уже переведён: `src/i18n/en.json:338` `"err.code": "code {code}"`, `src/i18n/ru.json:338` `"код {code}"` (де/фр тоже переведены). Реальная причина текущего дефекта не в отсутствии такого ключа, а в порядке проверок внутри `_errText` (`houseplan-card.ts:9528`: `if (e.message) return e.message;` стоит РАНЬШЕ ветки `err.code`) — значит для любой ошибки, где бэкенд шлёт одновременно код и `message`, независимо от известности кода, сейчас показывается сырое английское `message`. AC6 («неизвестный код → локализованный fallback») не сможет быть проверен юнит-тестом однозначно, пока ТЗ не решит: (а) переиспользуется `err.code`/`err.unknown` без новых ключей — тогда раздел i18n тривиален («новых ключей нет, порядок проверок в `_errText` меняется на code-first») — либо (б) вводится новый текст — тогда нужны конкретные en+ru строки. Без явного выбора ревьюер кода не сможет сверить AC6 с намерением автора, а автор рискует написать тест под собственную догадку, которую я не смогу отличить от решения. **Как чинится в скоупе:** добавить короткий раздел «i18n» с явным решением (рекомендация — вариант (а), ключ уже есть и уже переведён на 4 языка) и одной строкой описать смену порядка проверок в `_errText` (code-first → message → error → JSON). ## Low (снимаю с записью, не блокирует) Разделы «UX» и «Модель данных и миграция», формально обязательные по PROCESS.md §7.1 как отдельные заголовки, в файле не оформлены как таковые — их содержание фактически присутствует, но разбросано («Что человек увидит до и после» покрывает UX: новых диалогов/интеракций нет, меняется только текст; «DoR-примечания» закрывает миграцию: единственное затронутое поле — формат `message` двух кодов, обратная совместимость на одну бету). Контент по существу верный и достаточный, поэтому не поднимаю до Medium — прошу консолидировать при следующей правке ради дословного соответствия §7.1, но это не требует нового цикла ради одного этого пункта. ## Что проверено и корректно - **Сценарий** и **«что человек увидит до и после»** — на месте, продуктовые, без терминов реализации; соответствуют J-рядам SCOPE.md лишь косвенно (инженерное качество — не отдельная строка core user jobs), но решение вести эту работу и её ценностная оценка (3/10 пользователю, 9/10 разработке) уже приняты владельцем в комментариях issue — не переоткрываю; - **Скоуп/не-скоуп** разделены явно, «следующая ступень» зафиксирована текстом, а не памятью; - **Контракт поведения**: код и структура успешных ответов не меняются; единственное видимое изменение — формат `message` двух кодов на JSON, риск для рассинхронизированной пары фронт/бэк одной беты назван и обоснован явно, а не спрятан; - **AC1–AC4, AC6, AC7** — однозначны, у каждого назван способ доказательства (CI/локально/юнит), и по каждому видно, как тест умеет упасть (AC1 — «подмена baseline на большее число», AC2 — «удаление homeassistant из шага установки», AC4 — «контракт-тест падает при сужении списка», AC6 — м1/м2 мутанты в разделе «План автотестов»); - **Риски** называют главный поведенческий риск (B023-фиксы) и версийный разъезд (3.10 песочница / 3.13 CI) без сокрытия; - **Откат** — `git revert`, без потери данных, явно; - **Release-артефакты** — оба changelog, `ARCHITECTURE.md`, `USER-GUIDE.ru` §Troubleshooting — названы; - **Принятые предположения** — присутствуют отдельным блоком, включая ту же идею про `backup.error.` как общее пространство кодов, которую находка Medium 2 просит явно продолжить в раздел i18n; - порог coverage 90→95 сознательно вынесен в отдельные trivial-issues — разумно, механизм этой ступени уже будет их принудительно проверять. ## Чего не проверял и почему - `npx tsc --noEmit`, `npm test`, `npm run build` + сверка копий бандла, `node scripts/check-docs.mjs`, `node scripts/model-invariants.mjs` — диф этого раунда состоит из одного файла `docs/specs/042-*.md` (класс C), в `src/**` и `custom_components/**/*.py` изменений нет; прогон гейтов кода на неизменном коде не даёт сигнала по существу ТЗ и не входит в предмет ревью ТЗ (PROCESS.md §2.4 против §2.7); - браузерные смоки, `golden:verify`, `pytest tests_backend`, performance — та же причина, плюс на этом этапе AC не привязаны к конкретному коду, который можно было бы прогнать; - фактическое покрытие 89.1% и «pure 240/0» из измерений владельца — не переснимал (нужен рабочий HA-harness/`pytest-cov`, которых в песочнице нет и установка которых для разового замера ушла бы за рамки ревью ТЗ). Считаю косвенным подтверждением точное совпадение независимо перепроверенных ruff-цифр (333/291/17/13/6/2/2) — метод измерения автора доверия заслуживает; - осуществимость конкретного стартового allowlist mypy strict (`const`, `projection`, `coordinate_canonicalization`, `frontend_asset_manifest`, `junction_limits`, `plans` + часть `validation`/`wall_segment_model`/ `geometry_migration`) — ТЗ само откладывает точный финальный список «по факту зелени» в handoff, это законное «assumed, решается в реализации», не спец-пробел. ## Итог Механизм в целом обоснован и предметен — почти все числа и ссылки в ТЗ проверяются на HEAD `dev` дословно, что необычно хорошо для спека такого объёма. Возврат — по двум находкам Medium в скоупе задачи: AC5 нужно дотянуть до кодов, приходящих через `err.code` (включая оба флагманских кода блока 5), и явно решить/записать пункт i18n. Обе решаются добавлением текста в тот же файл, без пересмотра архитектуры и без нового цикла владельца.