21 KiB
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 1actual >= baseline - 0.1, см.custom_componentsjob логи; механизм описан в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иimportreordering — механические. Подтверждено тем, что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 (job100431708612), это не догадка, но не построчный разбор. 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 → в задаче
Материал раунда
- Ветка:
issue/51-custom-decor-images, коммит043aa0d826cf— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
576c3e884af13ca0395b0cf943111e3654f3edddgit log --all --format='%H %T' | grep 576c3e884af1