docs: #42 spec revision 2 — mechanism + baseline stage, measured facts

User-Visible: no
Issue: #42
This commit is contained in:
Codex
2026-08-30 15:28:52 +03:00
parent f7ac00f895
commit 582d673a2d
+161 -42
View File
@@ -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.<code>`, 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.<code>` в 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.<code>`.
- **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.<code>` — существующее пространство ключей для всех
WS-ошибок (не только бэкапов) — так уже используется фронтом; переименование
пространства — вне скоупа.
- Формат JSON-details фиксируется этим ТЗ как контракт двух кодов; общий
механизм details для ВСЕХ кодов — следующая ступень при необходимости.