Files
houseplan-card/docs/reviews/CODE-REVIEW-51-r3.md
T
2026-09-02 21:56:04 +00:00

238 lines
21 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.
# CODE-REVIEW — issue #51 «Custom decor images» · заход r3
- Issue: https://github.com/Matysh/houseplan-card/issues/51
- Материал раунда: `git diff 042af520fe4f6cc47d78ae6ccbb105580382e961..HEAD`
(материал r2 объявлен в `docs/reviews/CODE-REVIEW-51-r2.md`).
- SHA материала: `043aa0d826cf1e1490a02feb4cc28f50cb96245b` (сверено с
`git rev-parse HEAD` — совпадает).
- SHA предыдущего раунда (r2): `042af520fe4f6cc47d78ae6ccbb105580382e961` —
жив, `git cat-file -t` → `commit`, `git merge-base --is-ancestor` подтверждает:
прямой предок `HEAD`, ребейза не было.
- Трейлеры всех пяти коммитов дельты (`5a93fc2a`, `a5f28c3a`, `db08b92a`,
`424a133b`, `043aa0d8`) — `Issue: #51`, `User-Visible: no`. Корректно: ни
один не меняет наблюдаемое пользователем поведение (типизация без изменения
рантайма, ruff-лint, публикация документа r2, пересъёмка отпечатка
скриншотов, регенерация `config-schema.json`) — правок в `docs/CHANGELOG*`
не требуется и не сделано.
## Скоуп раунда
Единственная предметная находка r2 (Medium, в скоупе: устаревший
`scripts/config-schema.json`) закрыта одним коммитом. Дельта — 9 предметных
файлов (без учёта бандлов), +361/-28: регенерация манифеста схемы,
`ruff`-порядок импортов в трёх backend-модулях, типизация трёх frontend-мест
(`decor-assets.ts`, `houseplan-editor-runtime.ts`, `space-render.ts`, ранее
помеченных `any`), обновлённый отпечаток документационных скриншотов и
публикация `CODE-REVIEW-51-r2.md`. Ребейза на ушедший вперёд `dev` не было,
контракт поведения не менялся, новая подсистема не задета, объём дельты
(9 файлов) на порядок меньше исходной задачи (63 файла) — критерий «разбор
остаётся полным» не выполняется, сокращение объёма правомерно.
Прочитал все пять диффов не-бандловых файлов построчно (см. «Проверено и
корректно») — это не декларативная типизация: `any` заменён на конкретные
локальные типы (`ContentItem`, `DecorAssetConfig`), добавлен non-null
assertion (`shape.asset_id!`) там, где рантайм уже гарантировал непустое
значение (тот же `if` guard, что и раньше), поведение не изменилось.
## Закрытие раунда r2
| Находка r2 | Чем закрыта | Где это видно |
|---|---|---|
| **Medium (в скоупе)** — `scripts/config-schema.json` устарел, `tests_backend/test_config_schema_manifest.py::test_issue_33_manifest_is_fresh_and_deterministic` красный после того, как `38205d87` добавил decor kind `image` в `validation.py` | `scripts/config-schema.json` регенерирован штатным `scripts/dump-config-schema.py`; добавлены 76 строк контракта `config.spaces[].decor[]<image>.*` | `scripts/config-schema.json` (диф `042af520..043aa0d8`); **воспроизведено лично**: `python3 scripts/dump-config-schema.py` (voluptuous доступен в песочнице без Home Assistant) перезаписал файл по тому же пути — `git status`/`git diff` после прогона пустые, то есть закоммиченный манифест побайтово совпадает со свежесгенерированным — детерминированность подтверждена, не только словом автора |
Единственная находка r2 закрыта предметно.
## Унаследовано из r2
Без повторной проверки приняты как есть — материал `docs/reviews/CODE-REVIEW-51-r2.md`
на SHA `042af520fe4f6cc47d78ae6ccbb105580382e961`, поскольку дельта r3 их код
не задевает:
- Весь High/Medium/Low реестр r1, закрытый и адверсариально проверенный в r2
(UTF-16/32 DTD/entity SVG guard, `smoke_decor.mjs`, единая
`projectDecorImage`, positive/negative resolve-кэш, touch pointerType,
SVG per-attribute bounds, `javascript:`/`data:`-substring — сознательно
оставлено Low).
- AC1, AC2, AC4, AC6, AC8–AC13 — унаследованы из r1 через r2, дельта r3 их не
трогает.
- Golden (`npm run golden:verify`, 153/153 в r1) и полный HA-harness — не
перепрогонял: дельта r3 не меняет визуальные числа и не меняет вызывающий
контракт HA-специфичных тестов (только порядок импортов и типы).
- Bundle budget — известный долг #367 (запас < 15000 Б) остаётся, число
практически не изменилось (291041 Б → 291018 Б).
## Как проверялось (гейты этого раунда)
Дешёвые гейты прогнал сам (зелёного Validate на `043aa0d8` на момент начала
ревью не было — CI ещё не завершился):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный |
| Unit (frontend) | `npm test` | 1778 tests, 1777 passed, 1 skipped, 0 failed |
| Build | `npm run build` | зелёный |
| Bundle sync (3 копии) | `npm run bundle:sync` | зелёный; `git status` после — чисто |
| Bundle budget | `npm run bundle:budget` | initial View 291018 B / 300000 B, запас 8982 Б — прошёл, тот же долг #367 |
| `no-new-any` по дельте раунда | `node scripts/no-new-any.mjs --base 042af520 --head HEAD` | зелёный: 16 добавленных строк в 3 файлах, новых `any` нет |
| `no-new-any` по всей задаче | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | зелёный: 808 строк в 10 файлах |
| Docs fingerprint (обязателен: дельта трогает `src/**`) | `node scripts/check-docs.mjs --external` | зелёный: «Documentation checks passed (7 files, 10 external links)», включая сверку `sourceFingerprint`/`captureScriptSha256`/побайтовых хешей 10 PNG |
| Регенерация config-schema (целевая проверка находки r2) | `python3 scripts/dump-config-schema.py` (voluptuous есть в песочнице) | манифест перезаписан по тому же пути; `git diff`/`git status` после — пусто → закоммиченный файл идентичен свежесгенерированному |
| Backend pure lint | `ruff check` | **не прогнан**: `ruff` не установлен в песочнице ревьюера и не ставится через `pip`/`python -m ruff` (нет пакета); см. ниже, чем компенсировано |
| **Backend: `pytest tests_backend` + coverage-порог** (обязателен: дельта трогает `.py`) | не смог прогнать локально: `pytest-homeassistant-custom-component==0.13.357`/`homeassistant==2026.8.3` требуют Python ≥3.14, в песочнице только 3.12.3, `pip install` за пином падает `No matching distribution` | **воспроизведено через реальный прогон CI на этом же SHA** — см. находку ниже, это не пропуск гейта, а его результат |
| Model invariants | не прогонял | дельта не трогает `marker.space`, `open_spans`, thickness-записи, `layout` — весь diff `042af520..HEAD` вне бандлов состоит из импортов, типов и данных манифеста/скриншотов, геометрии не касается |
| Целевые смоки/`smoke-select` | `node scripts/smoke-select.mjs --base 042af520 --head 043aa0d8` | «НЕОПРЕДЕЛЁННОСТЬ»: изменено 3 файла `src/**`, 5 символов на изменённых строках (`ContentItem`, `DECOR_ASSET_ID_RE`, `DecorAssetConfig`, `DecorShape`, `decorAssetIds`), ни один смок не привязан. Не прогонял: все пять символов — либо локальные типы, стираемые при компиляции (`ContentItem`, `DecorAssetConfig`, `DecorShape` — только аннотации, без изменения рантайма), либо неизменная сигнатура существующей функции (`decorAssetIds`, `DECOR_ASSET_ID_RE`), которую уже покрывает `test/decor-assets.test.mjs:18` (юнит зелёный в `npm test` выше) |
| Golden / HA full harness / performance | не прогонял | см. «Унаследовано из r2» — дельта не меняет визуальные числа и HA-специфичный контракт |
### Как проверялось: реальный CI-прогон на `043aa0d8`
Поскольку локальный backend-гейт недоступен (Python 3.14 нет в песочнице),
проверил статус актуального прогона `Проверка (CI)` для этого SHA напрямую
через `gh api`/`gh run view`:
- `run 33685317804`, `headSha 043aa0d826cf1e1490a02feb4cc28f50cb96245b`,
итог `completed / cancelled`.
- Job **«Бэкенд: pytest в Home Assistant»** (`100431708612`) — **failure**.
Сам `pytest tests_backend/ -q --cov=... --cov-branch` зелёный: `526 passed,
2 skipped`. Падает следующий шаг, сравнивающий покрытие с
`scripts/backend-coverage-baseline.txt`:
```
coverage: 86.9% (baseline 87.2%)
Process completed with exit code 1
```
(порог — `actual >= baseline - 0.1`, см. `custom_components` job логи;
механизм описан в `docs/specs/042-backend-engineering-quality.md` §4:
«одно число, шаг CI сравнивает: ниже baseline − 0.1 п.п. → красный»,
ratchet **не должен снижаться**).
- Остальные job'ы на этом SHA: «Предполётные проверки» — success (значит,
проверка свежести `config-schema.json` теперь тоже зелёная в CI, совпадает
с моим локальным воспроизведением находки r2); «Фронтенд: типы, юниты,
мутанты, синхрон бандла» — success; «Hassfest», «HACS» — success; смоки
шард 1/3 — success, шард 2/3 — cancelled (следствие общего fail-fast после
красного backend-job, не отдельная находка); Golden/perf-smoke — skipped
по той же причине.
## Находки
### Finding 1 (Medium, в скоупе) — backend coverage ratchet красный на `HEAD`: 86.9% < baseline 87.2% − 0.1
**Гейт:** `python -m pytest tests_backend -q --cov=custom_components/houseplan
--cov-branch` + шаг сравнения с `scripts/backend-coverage-baseline.txt` —
стандартный гейт код-ревью для задач, трогающих `custom_components/**/*.py`
(PROCESS.md §8), встроенный в тот же job CI. Все 526 тестов зелёные — падает
именно порог покрытия.
**Причина, воспроизведённая по логам реального прогона (не только словом
автора):** сравнил построчную таблицу `coverage --cov-report=term` на
`043aa0d8` (job `100431708612`) с последним зелёным прогоном этого же job на
`origin/dev` до фичи #51 (`cfa5c779`, job `100381072223`):
| Файл | dev (`cfa5c779`) | `HEAD` (`043aa0d8`) |
|---|---|---|
| `decor_assets.py` | файла не существовало | 292 стат., 71 непокрыт (**71%**) |
| `http_api.py` | 185 стат., 56 непокрыт (68%) | 304 стат., 87 непокрыт (69%) |
| `websocket_api.py` | 1069 стат., 155 непокрыт (84%) | 1145 стат., 169 непокрыт (83%) |
| **TOTAL проекта** | **87.2%** (зафиксировано как baseline) | **86.9%** |
Фича #51 добавила крупный новый модуль (`decor_assets.py` — SVG/raster
security-валидация, ровно тот код, где в r1 нашли обходимый DTD/entity-фильтр)
с покрытием заметно ниже среднего по проекту (71% против ~85-87%), плюс
расширила `http_api.py`/`websocket_api.py` без пропорционального прироста
тестов на новые ветки. Взвешенное среднее по всему backend просело на 0.3 п.п.
и вышло за допуск ratchet-гейта (0.1 п.п.).
**Не воспроизведено мной построчно (какие именно 71 из 292 statements
`decor_assets.py` не покрыты)** — `coverage.xml` в артефактах прогона не
публикуется, а локальный запуск `pytest --cov` недоступен: пин
`pytest-homeassistant-custom-component==0.13.357`/`homeassistant==2026.8.3`
требует Python ≥3.14 (см. `tests_backend/requirements.txt` — пины
преднамеренные, менять их ради ревью нельзя), в песочнице только 3.12.3;
`pip install -r tests_backend/requirements.txt` в отдельном venv (проверил)
падает `No matching distribution`. Табличные Miss-числа выше — это реальные
данные CI на этом SHA, не догадка, но точные номера строк я не устанавливал.
**Не архитектурный риск** — существующий malicious/regression corpus
`tests_backend/test_decor_assets.py` (27 тестов) уже ловит все находки r1/r2
(UTF-16/32 DTD/entity, oversized attrs, unit-interval). Но именно потому, что
это security-критичный модуль (метка `security` на issue, r1 уже нашёл
обходимый фильтр в непокрытом углу), 29% непокрытых statements в нём — это
плоскость, где обходы прячутся, а не косметика.
**В скоупе задачи, фикс не архитектурный:** добавить backend-тесты на
непокрытые ветки `decor_assets.py`/`http_api.py` (или обосновать `# pragma:
no cover` для реально недостижимого кода) до восстановления ≥87.1%; по
правилу ratchet baseline **нельзя** просто занизить — механизм специально
задуман монотонным (docs/specs/042 §4: «Пороги 90%→95%: … меняющие ОДНО число
… baseline»; вниз это число двигать не предполагается).
## Проверено и корректно
- Единственная находка r2 закрыта и лично перепроверена: регенерация
`scripts/config-schema.json` детерминирована (перегенерировал сам —
`git diff` пуст).
- Все пять правок дельты — типизация/порядок импортов без изменения
поведения: прочитал построчно `decor-assets.ts`, `houseplan-editor-runtime.ts`,
`space-render.ts`, `decor_assets.py`, `http_api.py`, `websocket_api.py`.
Non-null assertion (`shape.asset_id!`) и приведения типов (`as ContentItem[]`)
не меняют рантайм-путь — тот же `if`/`filter`-guard, что и раньше; `ruff`
и `import` reordering — механические. Подтверждено тем, что `npm test`
(1777) и `npx tsc --noEmit` зелёные на этом же дереве.
- `check-docs.mjs` подтверждает: отпечаток скриншотов пересчитан по всему
`src/**` (обязателен, т.к. дельта трогает `src/**`), 10/10 PNG совпадают
побайтово с принятыми — регрессии класса #230/#234/#237 в этом раунде нет.
- Три копии бандла синхронны (`bundle:sync` не изменил дерево), бюджет не
превышен (тот же известный долг #367).
- Реальный CI-прогон на `HEAD` подтверждает и «Предполётные проверки»
(включая свежесть `config-schema.json`) зелёными, и фронтенд-job зелёным —
независимое от меня подтверждение моих локальных гейтов.
## Чего не проверял
- **Точные номера непокрытых строк** `decor_assets.py`/`http_api.py` —
`coverage.xml` не публикуется как артефакт, локальный прогон недоступен
(Python 3.14 нет в песочнице). Табличные агрегаты Miss/Stmts взяты из
реального лога CI (job `100431708612`), это не догадка, но не построчный
разбор.
- `ruff check custom_components/houseplan` — пакет `ruff` не ставится в
песочнице (`pip`/`python -m ruff` не находят модуль); компенсировано тем,
что job «Предполётные проверки»/лint на актуальном `HEAD` в реальном CI —
success (виден в прогоне `33685317804`).
- Полный HA-harness (`test_ha_websocket.py`, `test_ha_import_export.py`,
`test_coordinate_canonicalization.py`) и Golden — не прогонял; дельта r3 их
не касается (см. «Унаследовано из r2»), для полноты вижу по реальному CI,
что до backend-failure остальные job'ы (Hassfest, HACS, фронтенд, 2 из 3
смок-шардов) зелёные.
- 4 из 5 символов `smoke-select`-выборки — не прогонял отдельно; все они
типо-стираемые аннотации либо неизменная сигнатура, уже покрытая юнитом
(см. таблицу гейтов). Отдельного смок-прогона не потребовалось.
- Model invariants — не прогонял: дельта не касается геометрии (сверено
чтением всего non-bundle diff).
## Вердикт
Единственная находка r2 закрыта предметно и лично перепроверена. Новая
находка этого раунда — Finding 1, Medium, в скоупе: backend coverage ratchet
красный на актуальном `HEAD` (86.9% < 87.2% − 0.1), обнаружено не декларацией
автора, а чтением реального прогона CI на этом же SHA (job
`100431708612`, run `33685317804`), поскольку локальный пере-прогон
`pytest tests_backend` в песочнице ревьюера недоступен по версии Python.
Причина — новый security-критичный модуль `decor_assets.py` с покрытием
заметно ниже среднего по проекту. Фикс не архитектурный (добавить backend-
тесты на непокрытые ветки), но не косметический как в r2 — реальный объём
работы. Без High это жёлтый вердикт по правилу §2.7: возврат автору, фикс
проходит следующий цикл ревью, отдельный issue не заводится (Medium в
скоупе).
Вердикт: жёлтый · заход r3 · блокирующих циклов 3/4 · High: 0 · Medium: 1 → в задаче
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/51-custom-decor-images`, коммит `043aa0d826cf` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `576c3e884af13ca0395b0cf943111e3654f3eddd`
```
git log --all --format='%H %T' | grep 576c3e884af1
```