Files
houseplan-card/docs/reviews/CODE-REVIEW-366-r1.md
T
2026-08-29 08:15:51 +00:00

12 KiB
Raw Blame History

CODE-REVIEW-366-r1

Issue: #366 · «Glow #20: движущиеся ворота на cover-сущности вызывают до ~100 пересчётов геометрии света за одно открытие» Этап: code · заход r1 · блокирующих циклов 0/2 (light track — бюджет 2 цикла) SHA ревью: 51657831e6c0a00336a099795943b9f002f1455f (HEAD ветки issue/366-cover-glow-quantum, диапазон origin/dev...HEAD) ТЗ: тело issue #366, ревизия 2 (принята SPEC-REVIEW-366-r2, зелёный вердикт, 0 Medium)

Скоуп

Один коммит, класс A+B+C+D:

  • src/logic.ts — новая константа OPENING_LIGHT_AMOUNT_QUANTUM = 0.05 и чистая функция quantizeOpeningLightAmount.
  • src/houseplan-card.ts — импорт функции и её применение в единственной точке сборки passageStates (:10381), которая питает и сигнатуру барьеров (:10383-10387), и длину светового выреза (:10408, :10420-10423).
  • test/logic.test.mjs — новый юнит на границы/зажимы/узлы + свип 0..1 через openingLightStateSignature.
  • scripts/mutation-gate.mjs — новый мутант opening-light-quantum-identity (квант → identity), guard --test-name-pattern="#366".
  • docs/LIGHT.md §Caching — упоминание сетки 0.05.
  • docs/CHANGELOG.md / docs/CHANGELOG.ru.md — запись в ## Unreleased в том же коммите, User-Visible: yes соблюдён.
  • docs/images/* + screenshots.json — пересчитанный sourceFingerprint из-за правки src/** (механический след check-docs, не отдельная работа).
  • dist/**, custom_components/houseplan/frontend/** — синхронный бандл.

Никакой геометрии комнат/стен, layout, marker.space, open_spans diff не трогает — инварианты модели (#254) на этот PR не применимы.

Как проверялось

Прогнано лично на этом SHA (зелёного Validate на SHA 51657831 нет):

Гейт Команда Результат
Typecheck npm run typecheck зелёный, без вывода
Юниты npm test 1534/0, skipped 1 — совпадает с заявленным в комментарии автора
Целевой юнит node --test --test-name-pattern="#366" test/logic.test.mjs 1/1 зелёный
Мутант (умеет падать) node scripts/mutation-gate.mjs --id=opening-light-quantum-identity тест покраснел, как обязан — AC5 подтверждён исполнением, не начитыванием
Сборка npm run build зелёная
Синхронность бандла npm run bundle:sync затем git status --porcelain пусто — dist, custom_components/houseplan/frontend, demo/srv/assets совпадают байт-в-байт с уже закоммиченным
Документация node scripts/check-docs.mjs Documentation checks passed (7 files, 10 external links) — обязателен, т.к. diff трогает src/**
Выбор смоков node scripts/smoke-select.mjs --base origin/dev --head HEAD «Прямое совпадение (2)»: smoke_opening_binding.mjs, smoke_registryless_opening.mjs (символ _openingAmt)
Смок (AC4, названный автором) node demo/smoke_glow.mjs зелёный, все проверки true/OK
Смок (прямое совпадение) node demo/smoke_opening_binding.mjs зелёный
Смок (прямое совпадение) node demo/smoke_registryless_opening.mjs зелёный
Видимый рендер npm run golden:verify (после свежего build+bundle:sync) все сценарии passed, 0 упоминаний fail; включая lighting-opaque-glow-two-doorways-dark, где cover.golden_door_half стоит на current_position: 50 — точный узел сетки 0.05, изображение не сдвинулось
Single-source-numbers node --test test/single-source-numbers.test.mjs зелёный (не относится напрямую к правке, но затронутое число — amount, видимое дважды — проверено ниже отдельно)

Не прогонял и почему:

  • Полный набор demo/smoke_*.mjs — smoke-select не назвал ничего сверх двух прямых совпадений плюс явно названный автором smoke_glow.mjs; diff — одна точка применения чистой функции, не новая подсистема. Все три релевантных смока зелёные.
  • npm run invariants — diff не трогает рёбра/толщину/layout/ссылки.
  • python -m pytest tests_backend — diff не трогает custom_components/**/*.py (единственный неродственный провал в моей среде — backend-test-guard.mjs внутри полного прогона mutation-gate, No module named pytest; это окружение без backend-venv, не относится к этой задаче, диагностировано и отброшено).
  • performance_smoke — не назван в AC, задача не меняет измеряемый в нём путь напрямую (только уменьшает частоту его срабатывания); предрелизный гейт прогонит его на полном наборе.
  • Полный node scripts/mutation-gate.mjs (без --id=) не гонял целиком — дорогой предрелизный гейт; целевой мутант прогнан отдельно и это то, что доказывает AC5.

Находки

Ни одной. High: 0, Medium: 0.

Что проверено и корректно

К1 (один квант, один источник). quantizeOpeningLightAmount — чистая функция: NaN→0 (через Number.isFinite guard), зажим в [0,1] до и после округления, округление к ближайшему узлу сетки 0.05. 0 и 1 — точные узлы арифметически (0/0.05=0, 1/0.05=20 — целые), не требуют отдельного кейса. Применена ровно в одной точке — houseplan-card.ts:10381, при сборке passageStates; это единственный производитель, который читает и сигнатура (:10383-10387), и вырез (:10408 openingLightApertureLength, :10420-10423 lightPartitionCuts) — по построению не могут разойтись (single-source принцип соблюдён: amount виден дважды — в сигнатуре кэша и в геометрии, — но у обоих ровно один источник, сама переменная amount в замыкании flatMap). Другие потребители _openingAmt — :8835 (изометрия), :12494, :12615 (рендер створки/подписи) — не квантуются, что и требовал контракт («анимация створки остаётся плавной»); проверено чтением всех вызовов _openingAmt в файле (4 сайта, один изменён).

К2 (≤21 сигнатура). Математически: узлов сетки 0.05 на [0,1] ровно 21 (0, 0.05, …, 1.00). Юнит-свип шагом 0.001 подтверждает исполнением, не только по построению.

AC1 — юнит на границы/зажимы/узлы — присутствует и зелёный, воспроизвёл локально.

AC2 — свип-юнит на ≤21 сигнатуру — присутствует, зелёный, воспроизвёл; дополнительно проверил, что мутант «квант→identity» красит именно этот ассерт (тест покраснел, как обязан).

AC3 (binary-двери байт-в-байт) — юнит фиксирует точные строки door:0.000 / door:1.000 и то, что quantizeOpeningLightAmount(1) даёт ту же сигнатуру, что и неквантованная 1. Формат сигнатуры (toFixed(3), logic.ts:371) не менялся.

AC4 (существующие смоки/golden #20 зелёные без правки ассертов) — smoke_glow.mjs, golden:verify (полный набор, включая сценарий с cover на 50%) зелёные; ни один ассерт в этих файлах не тронут diff'ом (проверено — diff не касается demo/** кроме mutation-gate.mjs).

AC5 (мутант красит юнит) — прогнан лично, поймано 1/1.

Трейлеры и changelog. Issue: #366, User-Visible: yes на коммите; docs/CHANGELOG.md и docs/CHANGELOG.ru.md правлены в этом же коммите — требование §Commits соблюдено. Формулировки на пользовательском языке («не дёргают карточку», «ступает по сетке 5%») без утечки реализации.

docs/LIGHT.md — Low из спек-ревью (r1-L2/r2-L2′: сетка квантования должна попасть в LIGHT.md тем же коммитом) закрыт: §Caching описывает сетку 0.05 и ссылку на #366.

Продуктовая рамка. Правка обслуживает J1 (живой обзор состояния дома) — устраняет визуальные подёргивания всей карточки на wall-планшете во время движения ворот, что прямо противоречит "at a glance" ценности продукта. Скоуп не расширен: чистая квантовая функция в одной точке конвейера, ничего геометрического или конфигурационного не тронуто. Решение (квант 0.05, а не debounce) — принятое предположение спек-ревью r1-M2, с ценой отката в одну константу; реализация не отступает от контракта ТЗ.

Чего не проверял

  • Ручного тестирования на реальном wall-планшете нет (не входит в цикл ревью); визуальная неразличимость шага 5% принята как предположение ещё на этапе спеки (r1-M2), не переоценивается здесь заново — это продуктовый вопрос, а не технический.
  • Не мерил фактическое число пересчётов на живом cover со стрим-обновлениями позиции (нет интеграционного стенда HA) — К2 доказан математически и юнит-свипом, что признано в ТЗ достаточным доказательством (юнит вместо browser-профиля).
  • Полный demo/smoke_*.mjs (201 файл) и performance_smoke — не прогонял, обоснование выше в разделе «Как проверялось».
  • tests_backend — не прогонял, diff не трогает Python.

Вердикт

Зелёный. Технический контракт реализован ровно как специфицирован в ревизии 2 ТЗ, все AC доказаны исполнением (не только чтением), затронутые смоки и golden зелёные, трейлеры и changelog на месте, бандл синхронен.