diff --git a/docs/specs/042-backend-engineering-quality.md b/docs/specs/042-backend-engineering-quality.md index a3f18d5a..4457ccf4 100644 --- a/docs/specs/042-backend-engineering-quality.md +++ b/docs/specs/042-backend-engineering-quality.md @@ -1,61 +1,180 @@ # ТЗ #42 — Измеряемое инженерное качество backend - Issue: https://github.com/Matysh/houseplan-card/issues/42 -- Приоритет: P2 -- Статус ТЗ: ready for implementation by incremental gates -- Тип: infra/tests, без изменения успешных пользовательских сценариев +- Приоритет: P2, tests/tech-debt; полный трек (backend class A + видимое + поведение ошибок — решение аналитики 2026-08-15) +- Ревизия: 2 (2026-08-30) — актуализация замерами исполнением; issue + реализуется механизмом + ступенью baseline, пороги — последующие trivial +- Тип: infra/tests + один видимый пользователю блок (тексты ошибок) -## Цель +## Замеры ревизии (HEAD, песочница) -Зафиксировать честное покрытие, строгую типизацию и стабильный error contract -backend integration без массового formatting rewrite. +Pure-coverage 89.1% (2377/258); HA-модули (~1.5К stmts, включая +import_export 1065) измеримы только в CI. Ruff E,F,B,I: 333 (E501 — 291, +содержательных 42). Ни одного requirements/pyproject в репо; зависимости +бэкенд-CI — строка в validate.yml + зеркало в mutation-gate.yml. WS-коды +де-факто стабильны и фронт мапит по code (`backup.error.`, 26 ключей +en/ru); дыры: сырой английский message в fallback и regex-парсинг message у +`invalid_passage_fields`/`invalid_partition_opening_jamb_margin`. -## Coverage +## Сценарий -- Добавить pinned `pytest-cov`; CI запускает Python 3.13 HA harness и pure tests - одним coverage combine workflow. -- Baseline публикуется по каждому `custom_components/houseplan/*.py` с branch - coverage. Первое включение не скрывает skipped HA tests. -- Gate вводится ступенями: не ниже baseline → 90% → минимум 95% executable - lines и согласованный branch threshold. Generated/frontend bundle исключён. -- `coverage.xml` artifact и human summary; новые/изменённые строки требуют 100% - либо documented pragma для unreachable defensive branch. +Разработчик ломает покрытие или типы бэкенда — CI краснеет с конкретным +модулем и числом. Пользователь получает ошибку от бэкенда — видит +локализованный текст по стабильному коду; неизвестный код даёт общий +локализованный fallback с кодом, а не сырую английскую фразу. -## Typing +## Что человек увидит до и после -Pyright/mypy strict включается per-module allowlist: +Почти ничего: единственная видимая часть — тексты редких ошибок. **До**: +неизвестная ошибка = английская message; два кода парсятся regex'ом из +английской строки. **После**: локализованный fallback с кодом; structured +JSON-details. Всё остальное — инженерные гейты. -1. `validation.py`, `store.py`, auth/const; -2. websocket request/result boundaries; -3. repairs/diagnostics/system_health; -4. trails/runtime. +## Скоуп (одна ступень, пять блоков) -HA dynamic APIs изолируются typed Protocol/adapter, а не `Any` по всему модулю. -Allowlist только уменьшается. +### 1. Tooling-фундамент -## Lint/format +`pyproject.toml` (первый в репо): метаданные не нужны — только конфиг +инструментов. Зависимости бэкенд-теста выносятся в +`requirements_test.txt` (pinned); validate.yml и зеркало в mutation-gate.yml +ставят из него (одна точка правды вместо двух строк). -Ruff (или один выбранный tool) с narrow rule set: errors/imports/bugbear и -format-check только для новых/затронутых Python файлов. Отдельный mechanical -PR может нормализовать остальное; feature diff не содержит repo-wide rewrite. +### 2. Lint (ruff, narrow) -## WebSocket error contract +Конфиг в pyproject: `select = ["E", "F", "B", "I"]`, +`ignore = ["E501"]` (291 длинная строка — НЕ переписываются: «без массового +rewrite»), target py313. Чинятся 42 содержательных нарушения: I001/F401/ +F841/E731/B905 — механически; **B023 (×17, loop-var в замыкании) — каждый +случай разбирается отдельно**: реальная гонка → фикс с юнитом, доказанная +синхронность → `# noqa: B023` с причиной в комментарии. CI-джоба +`ruff check custom_components/houseplan` в validate.yml (backend). -Все user-facing failures имеют stable code enum, safe developer message и -optional structured details без персональных данных. Frontend mapping ru/en -не сравнивает английские message strings. Unknown code получает общий fallback. +### 3. Typing (mypy strict, растущий allowlist) -## Quality Scale docs +`[tool.mypy]` per-module: strict для стартового списка достижимых +pure-модулей — `const`, `projection`, `coordinate_canonicalization`, +`frontend_asset_manifest`, `junction_limits`, `plans` (+ те из +`validation`/`wall_segment_model`/`geometry_migration`, что пройдут без +каскадного рефакторинга — финальный список фиксируется по факту зелени и +называется в handoff). Список strict-модулей может только РАСТИ — +контракт-тест сравнивает конфиг с committed-списком и падает при удалении. +HA-boundary модули (websocket/http/store/repairs/…) — вне ступени (нужны +stubs HA, CI-итерации) — следующая ступень, зафиксировано здесь. -Добавить troubleshooting, examples и проверить manifest/quality_scale claims. -Нельзя отмечать rule выполненным только наличием файла — acceptance следует HA -rule text и CI evidence. +### 4. Coverage (механизм + baseline-гейт) -## Приёмка +- validate.yml backend: pytest → `--cov=custom_components/houseplan + --cov-branch --cov-report=xml --cov-report=term`; артефакт coverage.xml. +- `scripts/backend-coverage-baseline.txt` — одно число (стартовое = + фактический общий % CI-прогона pure+harness, снимается первым прогоном + ветки); шаг CI сравнивает: ниже baseline − 0.1 п.п. → красный. +- Защита от тихого скипа harness: шаг до pytest — `python -c "import + homeassistant"` + после collect: количество собранных + `tests_backend/test_ha_*` ≥ 50, иначе красный. +- Пороги 90% → 95%: последующие trivial-issues, меняющие ОДНО число в + baseline-файле (механизм этой ступени их уже enforce'ит). Приёмка issue + «≥95%» достигается той лестницей; данная ступень сдаёт механизм + + «не ниже baseline», и это отражено в квалификации quality_scale + (test-coverage остаётся `todo` с прогресс-ссылкой). -- CI нельзя пройти с silently skipped HA harness; -- coverage ≥95% executable lines после staged rollout; -- strict module allowlist и lint gates зелёные; -- WS tests проверяют code + frontend localization; -- docs examples исполняемы/проверяемы; -- изменения tooling не меняют stored data/runtime result. +### 5. WS error contract + доки + +- `const.py`: `ERROR_CODES` — frozenset всех стабильных кодов (собранных + фактически по websocket_api + коды исключений). Контракт-тест (pure, + скан исходника): каждый литерал `send_error(...)`-кода ∈ ERROR_CODES; + каждый код имеет i18n-ключ `backup.error.` в en (полнота словарей — + существующий паритет-гейт). +- Structured details: `invalid_passage_fields` и + `invalid_partition_opening_jamb_margin` шлют message + JSON-строкой (`{"space":…,"opening":…,"fields":…}`); фронт парсит + JSON.parse с fallback на прежний regex (совместимость со старым бэкендом + одной беты). Regex-ветка помечена deprecated-комментарием с датой + удаления. +- Fallback `_errText`: известный код → i18n; неизвестный → общий + локализованный текст + код; сырой английский `e.message` в UI не + показывается (уходит в console.warn для отладки). +- Доки: USER-GUIDE.ru получает паритетный §Troubleshooting (перевод §22); + quality_scale.yaml: `docs-troubleshooting` → done, + `docs-examples` → done ТОЛЬКО если текст HA-правила фактически + удовлетворён существующими YAML-примерами гайда (проверка по тексту + правила; иначе остаётся todo с причиной); `strict-typing` — остаётся + todo с прогрессом (ступень). + +## Не-скоуп (следующая ступень, зафиксировано) + +Strict typing HA-boundary модулей; пороги coverage 90/95; формат-проверка +всего репо; массовая нормализация E501; перевод остальных секций гайдов. + +## Контракт поведения + +Tooling не меняет stored data и успешные пользовательские сценарии. +Единственное видимое изменение — тексты ошибок (блок 5): коды и структура +ответов бэкенда с существующими кодами НЕ меняются (message двух кодов +меняет ФОРМАТ на JSON — фронт совместим в обе стороны одну бету). + +## Критерии приёмки + +- **AC1** (CI): джоба backend публикует coverage.xml + summary; подмена + baseline на большее число → красный шаг (доказательство прогоном ветки). +- **AC2** (CI): удаление homeassistant из шага установки или фильтр + test_ha_* → красный ещё до pytest / на collect-пороге. +- **AC3** (локально): `ruff check` чист на выбранном наборе; каждый + `noqa: B023` несёт объяснение (контракт-тест: noqa без текста запрещён). +- **AC4** (локально): mypy strict зелёный на стартовом allowlist; + контракт-тест падает при СУЖЕНИИ списка. +- **AC5** (юнит): ERROR_CODES ⊇ все коды send_error (скан исходника); + каждый код имеет en-ключ `backup.error.`. +- **AC6** (юнит фронта): JSON-message двух кодов парсится в structured + details; старый regex-формат по-прежнему принимается; неизвестный код → + локализованный fallback, английский message не попадает в DOM. +- **AC7**: полный гейт; pytest 240/0 pure; бюджет ≈ без изменений (фронт + меняет только обработку ошибок). + +## План автотестов + +- Юниты фронта: AC6 (парсер details + fallback) — test/logic или + error-текст тесты. +- Pure-pytest: AC5-скан; существующие 240 не слабеют. +- Контракт-тесты: AC3-noqa, AC4-allowlist (читают pyproject/исходники). +- Мутанты: м1 — удалить код из ERROR_CODES → красный AC5; + м2 — вернуть regex-first парсинг (сломать JSON-ветку) → красный AC6. +- CI-доказательства AC1/AC2 — прогоном ветки, фиксируются в handoff. + +## Риски + +- B023-фиксы — единственные поведенческие: каждый со своим юнитом или + обоснованным noqa. +- Python 3.10 (песочница) vs 3.13 (CI): ruff/mypy конфиг target 3.13, + локальная проверка на 3.10 — синтаксис кода уже совместим. +- JSON-message: старый фронт с новым бэком увидит JSON-строку в сыром + fallback → в пределах одной беты допустимо (пары версий фронт/бэк + обновляются вместе HACS'ом); отмечено в ченджлоге. + +## Откат + +`git revert`: конфиги/гейты исчезают, коды ошибок не менялись, формат +message двух кодов возвращается — фронт совместим (regex-ветка ещё жива). +Потери данных нет. + +**DoR-примечания:** миграция/compatibility — только формат message двух +кодов (двусторонняя совместимость на бету); touch — не влияет; +производительность — не влияет (test/CI-time). + +## Release-артефакты + +- CHANGELOG×2: user-visible коротко (локализованный fallback ошибок), + остальное — инженерная запись. +- docs/ARCHITECTURE.md: раздел «Backend quality gates» (coverage baseline, + ruff, mypy allowlist, ERROR_CODES) со ссылками. +- USER-GUIDE.ru §Troubleshooting. + +## Принятые предположения + +- Baseline-число снимается ПЕРВЫМ CI-прогоном ветки и коммитится в неё же + до S7 (ревьюер видит фактическое значение). +- `backup.error.` — существующее пространство ключей для всех + WS-ошибок (не только бэкапов) — так уже используется фронтом; переименование + пространства — вне скоупа. +- Формат JSON-details фиксируется этим ТЗ как контракт двух кодов; общий + механизм details для ВСЕХ кодов — следующая ступень при необходимости.