Files
houseplan-card/docs/reviews/CODE-REVIEW-440-r1.md
T
claude[bot] 3fafbb392d
Проверка (CI) / Классификация изменённых файлов (push) Successful in 20s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 47s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 51s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 20s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 22s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 13m36s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 4m5s
docs: review document for #440
Issue: #440
User-Visible: no
2026-09-03 14:19:13 +00:00

243 lines
23 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-440-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/440
- Этап: код-ревью, заход r1, блокирующих циклов израсходовано 0 из 4 (лимит 4 — задача идёт full-треком)
- Материал: `git diff origin/dev...HEAD`, `git log --oneline origin/dev..HEAD`
- SHA материала (сверено `git rev-parse HEAD` непосредственно перед выводом): `5409f0b2e39f84d2e4ffd19a080b02b8c16849b8`
- ТЗ: `docs/specs/440-v171-beta2-polish.md` (SPEC-REVIEW-440-r1: зелёный, High 0 · Medium 0)
- Первый заход код-ревью — раздела «Унаследовано из r0» и «Закрытие раунда r0» не применяются, разбор полный.
## Скоуп диффа
5 коммитов на ветке `issue/440-audit-polish` (735710f1 ТЗ, 0f513ac8 doc, 53847e39
`fix: harden beta 2 audit paths` — User-Visible: yes, c958e243/5409f0b2
`test:`/`refactor:` — User-Visible: no, оба несут только внутреннее
переразложение уже написанного кода без изменения поведения). Затронуты все
семь пунктов аудита из тела issue:
1. `custom_components/houseplan/asset_integrity.py` — regular-file boundary в
`_signature()`, конечный `flight.event.wait(ASSET_INTEGRITY_FOLLOWER_TIMEOUT_SECONDS)`.
2. `src/houseplan-card.ts` — `_roomTipEnabledForPointer()` вызывает `_notePointer`
до проверки `show_room_tooltip`.
3. `src/houseplan-card.ts` — `_dangerConfirmLocaleGate` (getter) →
`_syncDangerConfirmLocaleGate()` (именованный метод) на всех трёх call site.
4. `custom_components/houseplan/decor_assets.py` + `http_api.py` — TOCTOU-устойчивый
`_physical_asset_inventory()`, статус `507` только для `capacity_exceeded`.
5. `test/space-card-audit-lows.test.mjs` — блок из пяти source-regex по AC5 #434
удалён, заменён отрицательным smoke-кейсом + двумя новыми мутантами.
6. `tests_backend/test_coordinate_canonicalization_pure.py` — новый HA-независимый
модуль, мутанты `image-box-python-canonicalization-omitted` и
`python-lattice-round-truncates` перенаправлены на него.
7. Пункт (е) — только текст ТЗ/issue, кодовых изменений нет; проверено сверкой
заявления с фактическим отсутствием диффа по #429/#430.
Плюс сгенерированный класс D (`dist/**`, `custom_components/houseplan/frontend/**`)
и документация (`ARCHITECTURE.md`, `TESTING.md`, оба `CHANGELOG*`,
`docs/specs/README.md`, `docs/images/screenshots.json`).
## Как проверялось
**Важное расхождение с вводной:** вводная задача утверждает «зелёного Validate
на этом SHA нет». Это неверно — на точном HEAD SHA `5409f0b2` найден и
проверен `gh run view 33764443449` (workflow «Проверка (CI)», событие `push`,
`headSha == 5409f0b2e39f84d2e4ffd19a080b02b8c16849b8`), **все 13 джобов
`completed success`**: предполётные проверки (docs/provenance/process-gate),
HACS, hassfest, frontend (typecheck/юниты/no-new-any/bundle-sync), backend
pytest под настоящей Home Assistant (`586 passed, 2 skipped`), golden, perf-smoke,
и все 3 шарда браузерных смоков + агрегирующий джоб. Указываю это отдельно,
потому что вводная инструкция прямо утверждает обратное — доверился не
заявлению, а `gh run view`/`gh api .../logs`.
Несмотря на найденный зелёный CI, дешёвые гейты и целевые негативные пробы
прогнаны самостоятельно (окружение — линуксовый sandbox без предустановленного
Home Assistant, но с сетью: `pytest`/`voluptuous` доустановлены `pip3 install`):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc --noEmit` | зелёный, без вывода |
| Unit (frontend) | `npm test` | `1819 pass, 1 skip, 0 fail` (совпадает с хендоффом автора) |
| Build + 3 копии бандла | `npm run build && npm run bundle:sync` | пересборка байт-в-байт совпала с закоммиченным деревом (`git status --short` пуст после пересборки) |
| Docs fingerprint | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 10 external links)` — обязателен, диффом задет `src/**` |
| Smoke-выбор | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 7 прямых совпадений (`smoke_decor`, `smoke_feedback_v2`, `smoke_room_tooltip_toggle`, `smoke_touch_tips`, `smoke_room_climate_placement`, `smoke_room_settings`, `smoke_ux_fixes`) + 25 слабых на `_mode` |
| Целевые browser smokes | `node demo/smoke_room_tooltip_toggle.mjs`, `node demo/smoke_danger_confirm_branches.mjs`, `node demo/smoke_space_card_decor_capability.mjs` | все три `OK`, все ключи `true` |
| Чистый Python-модуль без HA | `python3 -m pytest tests_backend/test_coordinate_canonicalization_pure.py -q` | `4 passed` — сам факт исполнения (не skip) доказывает AC6 |
| Backend полный доступный локально набор | `python3 -m pytest tests_backend -q` (с доустановленными `pytest`+`voluptuous`, без `homeassistant`) | `328 passed, 4 skipped` — HA-зависимые файлы честно пропущены `conftest.py`, не среди них ничего из диффа AC1/AC4/AC6 |
| Целевые новые pytest-тесты | `pytest tests_backend/test_decor_assets.py -k "integrity_verifier_rejects_non_regular or integrity_follower_has_a_bounded_wait or integrity_verifier_rejects_fifo or physical_inventory_skips or physical_inventory_treats_disappearing"` | `5 passed` (включая реальный `os.mkfifo` на Linux) |
Мутанты из `scripts/mutation-gate.mjs`, прогнанные точечно по `--id=` (каждый
поднимает свой worktree, пересобирает бандл/test-build и гоняет ровно один
guard — см. таблицу защитных доказательств ниже) — 10 из 10 новых/переименованных
идентификаторов дают ожидаемый результат. Единственное исключение — уже
существовавший до #440 мутант `danger-confirm-warm-language-guard-removed`,
разобран в разделе «Находки».
**Что не проверялось и почему:**
- `demo/smoke_*` вне выбора smoke-select и трёх целевых (216 всего) — не
прогнаны локально; CI-джоб «Смоки: все шарды зелёные» на этом SHA закрывает
этот пробел авторитетно (все 3 шарда `success`).
- `npm run golden:verify` не гонял локально — CI job «Golden-кадры против
принятых эталонов» зелёный на этом SHA, а дифф не меняет визуальный вывод
(только backend-статусы и internal pointer bookkeeping).
- `performance_smoke` — не в AC, не запускал; CI-job зелёный.
- Полный `pytest tests_backend` с настоящей Home Assistant — недоступно в
песочнице (нет `.venv-backend`); закрыто CI-логом `586 passed, 2 skipped`.
- Полный прогон `scripts/mutation-gate.mjs` без `--id` (весь реестр, сотни
worktree-пересборок) не запускал — непропорционально объёму диффа; §8
разрешает точечный прогон при код-ревью.
## Таблица защитных доказательств (PROCESS.md §2.7)
| AC | Чем доказан | Чем краснеет (проверено лично) |
|---|---|---|
| AC1 (regular-file boundary) | `test_integrity_verifier_rejects_non_regular_files_before_hashing`, `test_integrity_verifier_rejects_fifo_without_opening_it` | `--id=asset-integrity-non-regular-file-admitted` → `тест покраснел, как обязан` |
| AC1 (follower bounded wait) | `test_integrity_follower_has_a_bounded_wait` (инъекция `event_factory`, без реального ожидания) | `--id=asset-integrity-follower-waits-forever` → `тест покраснел, как обязан` |
| AC2 (pointer modality при выключенном tooltip) | `demo/smoke_room_tooltip_toggle.mjs` (`disabledPenMoveClearsMouseHover`, `disabledTouchMoveClearsMouseHover`) | `--id=room-tooltip-off-skips-pointer-modality` → `тест покраснел, как обязан` |
| AC3 (locale gate не читается как getter) | `demo/smoke_danger_confirm_branches.mjs` (`warmLanguageGateRefusesImmediately` и др., 18 ключей) | `--id=danger-confirm-uses-last-rendered-language-gate` → покраснел; `--id=danger-confirm-warm-transition-cancel-removed` → покраснел; `--id=danger-confirm-lost-space-transition-cancel-removed` → покраснел. Четвёртый мутант той же группы, `danger-confirm-warm-language-guard-removed`, НЕ покраснел — см. «Находки», это не регрессия #440 |
| AC4 (inventory race) | `test_physical_inventory_skips_entries_lost_during_stat`, `test_physical_inventory_treats_disappearing_root_as_empty` | `--id=decor-physical-inventory-follows-sidecars` → покраснел |
| AC4 (HTTP-статусы 507/400/500) | `test_decor_asset_upload_deduplicates_and_rejects_mime_spoofing` (три новые ветки: capacity/invalid/OSError) | `--id=decor-upload-invalid-image-reported-as-capacity` требует `tests_backend/test_ha_websocket.py`, который тянет настоящую `homeassistant` — недоступно в песочнице; **проверено чтением**: продакшн-патч `status = 507 if err.code == "capacity_exceeded" else 400` тривиален и прямо соответствует трём новым assert’ам в тесте; сам тест и три статуса подтверждены зелёным CI-джобом `Бэкенд: pytest в Home Assistant` (586 passed) на этом SHA |
| AC5 (static-card capability без source-regex) | `demo/smoke_space_card_decor_capability.mjs` (`capabilityOnlyUpgradeAdopted`, `oldBackendSkippedResolve` и др.) | `--id=space-card-decor-capability-downgrade-does-not-clear-assets` → покраснел; `--id=space-card-decor-capability-change-not-adopted` → покраснел |
| AC6 (Python canonicalization без HA) | `tests_backend/test_coordinate_canonicalization_pure.py`, реально исполняется без `homeassistant` в песочнице | `--id=image-box-python-canonicalization-omitted` → покраснел; `--id=python-lattice-round-truncates` → покраснел; оба прогона состоялись **без установленного `homeassistant`**, что и есть предмет AC6 |
## Находки
### Low — предсуществующий (не #440) неточный мутант, вне блокировки
`scripts/mutation-gate.mjs`, id `danger-confirm-warm-language-guard-removed`
(строки ~1062–1071). Патч убирает ранний `if (this._syncDangerConfirmLocaleGate()
=== 'warm') return Promise.resolve(false);` внутри `_confirmDanger()`, оставляя
только `void this._syncDangerConfirmLocaleGate();`. Комментарий `because`
утверждает, что без этой строки «a newly registered confirmation has no
rendered decision source and hangs». На практике (проверено: применил патч
вручную в отдельном клоне на `origin/dev`, пересобрал бандл, прогнал
`node demo/smoke_danger_confirm_branches.mjs`) смок остаётся зелёным —
`warmLanguageGateRefusesImmediately: true` не меняется. Причина: соседний,
независимый guard в `willUpdate()` (`this._dangerConfirm && (... ||
this._syncDangerConfirmLocaleGate() === 'warm')) { this._cancelDangerConfirm();
}`) в течение того же тика ловит только что зарегистрированный confirm и
резолвит его в `false` через отмену — второй слой защиты делает первый
избыточным в наблюдаемом поведении этого конкретного смока. Подтверждено
отдельно: мутант `danger-confirm-warm-transition-cancel-removed`, который как
раз снимает этот второй guard, ловится корректно.
**Не регрессия #440**: тот же результат воспроизводится на чистом
`origin/dev` (173707d8) с патчем под старым именем геттера
(`_dangerConfirmLocaleGate`) — #440 только переименовал геттер в метод,
семантика мутанта не изменилась. Предмет #417, не входит в перечисленную в
ТЗ #440 таблицу защитных доказательств для AC3.
**Итог: Low, без правки.** Реальный контракт (тёплый locale-gate не даёт
зависшего подтверждения) доказан рабочим соседним мутантом
(`danger-confirm-warm-transition-cancel-removed`) — дыры в защите нет, дыра
только в точности комментария одного конкретного мутанта. Правка комментария
или консолидация двух мутантов — за пределами скоупа #440 (задача явно
исключает «рефакторинг всего LanguageRuntime либо всех Lit render gates»);
подходящее место — issue про сам #417/mutation-gate, если владелец сочтёт
нужным его завести. Отдельный issue сейчас не создаю: находка Low, а не
Medium, и правило об обязательном issue (#202) касается только Medium вне
скоупа.
## Что проверено и корректно
- **AC1.** `_signature()` теперь бросает `OSError` до какого-либо чтения байт
для не-regular пути (переименование локальной `stat` → `current` устраняет
затенение только что импортированного модуля `stat` — самим диффом сделано
аккуратно). Второй `_signature()`-вызов после хеширования по-прежнему ловит
подмену/удаление во время чтения (не тронуто диффом, но проверено чтением:
`except Exception` в `verify()` перехватывает и второй OSError). Follower
ждёт `flight.event.wait(30.0)` вместо безусловного `wait()`; таймаут
проверен инъекцией фейкового `Event`, не реальным ожиданием 30 секунд.
- **AC2.** `_roomTipEnabledForPointer()` — единственная точка входа `tip()`
callback'а; `_notePointer` вызывается до проверки `show_room_tooltip`,
совпадает с контрактом `docs/TOUCH-SUPPORT.md` («Touch and pen input
immediately clear transient room and device hover… even when [tooltip]
off»). Повторный вызов `_notePointer` внутри `_showTip()` на пути с
включённым tooltip идемпотентен (тот же `pointerType`, `_tip`/`_hoverRoom`
уже приведены к нужному состоянию первым вызовом) — совпадает с явным
требованием ТЗ «повторный вызов… не должен менять наблюдаемое поведение».
Все три существующих site (`enterRoom`, capture-guard, `tip()`) используют
единый путь через `_notePointer`, дублирования логики модальности нет.
- **AC3.** Ровно три call site (`_confirmDanger`, `willUpdate`,
`_renderBody`) переведены с геттера на `_syncDangerConfirmLocaleGate()`;
`grep` не находит более ни одного обращения к старому имени в `src/`.
Смок `smoke_danger_confirm_branches.mjs` больше не продвигает состояние,
читая мутирующий геттер в цикле опроса (`while ... !card.inert` вместо
`while ... card._dangerConfirmLocaleGate !== 'warm'`) — именно та подмена
«наблюдаемое действие вместо чтения» из плана автотестов ТЗ.
- **AC4.** `_physical_asset_inventory()` — один `iterdir()`, try/except на
уровне корня (исчезнувший каталог → пустой список) и на уровне каждой
entry (`stat(follow_symlinks=False)` может кинуть OSError — кандидат
пропускается, соседи продолжают обрабатываться). `physical_asset_blobs`/
`physical_asset_usage` — тонкие обёртки над одним и тем же списком
(raньше два независимых обхода — теперь при исчезновении файла между
ними невозможно рассинхронизировать count/bytes). `http_api.py`
различает `capacity_exceeded` (507) от остальных `DecorAssetError` (400),
`OSError` остаётся 500 `io_error` — три ветки прямо протестированы в
`test_ha_websocket.py`.
- **AC5.** Пять удалённых regex-строк сопоставлены один-к-одному:
`config-store.test.mjs:17,20,22,34` (fresh config/get + localStorage never
seeds capability), `smoke_space_card_decor_capability.mjs`
(`oldBackendSkippedResolve`/`downgradeRevokesWithoutResolve` — clear+no
resolve; `sameEpochUsesCache` — revision-scoped cache;
`capabilityOnlyUpgradeAdopted` — новая строка диффа, единственный
недостающий кейс) и двумя новыми mutation id. `src/space-card.ts` в диффе
не тронут — расширено только покрытие уже реализованного поведения #434,
как и требует ТЗ.
- **AC6.** Новый `tests_backend/test_coordinate_canonicalization_pure.py`
импортирует только `custom_components.houseplan.coordinate_canonicalization`
(модуль без HA-зависимостей, подтверждено чтением файла — там нет
`import homeassistant`), реально выполняется в песочнице без
`homeassistant`. Старый `test_coordinate_canonicalization.py` сохранил
`pytest.importorskip("homeassistant")` и HA-зависимые тесты (`store`,
`virtual_lights`), дублирования тех же assert между файлами не нашёл.
Оба mutation guard (`image-box-python-canonicalization-omitted`,
`python-lattice-round-truncates`) перенаправлены на новый файл и красные
без HA — это и есть предмет AC6.
- **AC7.** Диффом не тронуты ни `custom_components/houseplan/store.py`, ни
схема, ни i18n-словари, ни `.github/workflows/**` — пункт (е) остаётся
чистым заявлением ТЗ без кода, как и заявлено.
- **AC8.** Typecheck/unit/build/bundle-sync/docs зелёные локально; backend
(без HA) зелёный локально; HA-зависимый backend + полный browser-smoke +
golden + perf-smoke зелёные на CI для точного SHA (см. «Как проверялось»).
- **Трейлеры.** Все 5 коммитов несут `Issue: #440` и корректный
`User-Visible:`. Единственный `User-Visible: yes` коммит (`53847e39`) несёт
правки в оба `docs/CHANGELOG*.md` в этом же коммите — сверено `git show
--stat`.
- **Одно число — один источник.** Дифф не вводит новых видимых пользователю
чисел (HTTP-статусы — не UI-величина); `test/single-source-numbers.test.mjs`
прошёл в составе `npm test`.
## Вердикт
Зелёный. High: 0 · Medium: 0 (единственная находка — Low, вне скоупа #440,
не блокирует, обоснованно не правится). Все восемь AC доказаны — либо
исполнимым тестом с подтверждённой способностью падать (лично прогнанные
негативные пробы для AC1/AC2/AC3/AC4-race/AC5/AC6), либо чтением с явной
пометкой там, где HA-зависимый прогон недоступен в песочнице, но закрыт
зелёным CI-логом на точном SHA (AC4 HTTP-статусы, AC8 HA-часть).
## Материал раунда
- Ветка: `issue/440-audit-polish`
- SHA: `5409f0b2e39f84d2e4ffd19a080b02b8c16849b8` (сверено `git rev-parse HEAD`
непосредственно перед выводом вердикта)
- Диапазон: `origin/dev...HEAD` (`origin/dev` = `173707d8`)
- CI: `gh run view 33764443449 --repo Matysh/houseplan-card` — `Проверка (CI)`,
`push`, headSha `5409f0b2e39f84d2e4ffd19a080b02b8c16849b8`, все джобы
`completed success`
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/440-audit-polish`, коммит `5409f0b2e39f` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `15f62eb4d88fffca6f2c18a20eb062957a7c8fb1`
```
git log --all --format='%H %T' | grep 15f62eb4d88f
```
- ТЗ `docs/specs/440-v171-beta2-polish.md`, блоб `0de292e2f2dc734f08a9f3eb4d37d2a6734da3e4`
```
git log --all --find-object=0de292e2f2dc734f08a9f3eb4d37d2a6734da3e4 -- docs/specs/440-v171-beta2-polish.md
```