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

23 KiB
Raw Blame History

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

Материал раунда

  • Ветка: 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