12 KiB
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 на месте, бандл синхронен.