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