18 KiB
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→ ровно 4todo: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-54OpeningPassageError.code = "invalid_passage_fields"иvalidation.py:63-72PartitionOpeningJambMarginError.code = "invalid_partition_opening_jamb_margin"— это ровно те два кода, вокруг которых построен весь блок 5 (JSON-details). Скан по литераламsend_error(...)их не найдёт — они читаются из атрибута класса, а не передаются строкой в месте вызова;validation.py:57-60PartitionOpeningHostError.code = "invalid_partition_opening_host"иvalidation.py:81-84WallModelClientOutdatedError.code = "wall_model_client_outdated"— то же;validation.py:35-40MarkerControlError.__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:56JunctionLimitError.code = f"junction_limit_{rule}"—ruleпробегает конечное множество ключей П1–П4 (junction_limits.py:427-430), но опять не литерал внутриsend_error(...).
Итого не меньше дюжины уже существующих кодов — включая оба кода, ради
которых написан блок 5 — невидимы для скана, читающего только литералы в
send_error(...). Реализация AC5 «в лоб» даст зелёный контракт-тест, который
ничего не доказывает для этих кодов: они не попадут в ERROR_CODES, останутся
без ключа backup.error.<code>, и _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.<code>как общее пространство кодов, которую находка 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. Обе решаются добавлением текста
в тот же файл, без пересмотра архитектуры и без нового цикла владельца.