diff --git a/docs/reviews/CODE-REVIEW-366-r1.md b/docs/reviews/CODE-REVIEW-366-r1.md new file mode 100644 index 00000000..c5939543 --- /dev/null +++ b/docs/reviews/CODE-REVIEW-366-r1.md @@ -0,0 +1,151 @@ +# 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 на месте, бандл синхронен.