Files
houseplan-card/docs/reviews/SPEC-REVIEW-42-r1.md
T
2026-08-30 12:39:05 +00:00

217 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.
# 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.<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. Обе решаются добавлением текста
в тот же файл, без пересмотра архитектуры и без нового цикла владельца.