Files
houseplan-card/docs/reviews/CODE-REVIEW-577-r1.md
T
2026-09-14 21:42:40 +00:00

273 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-577-r1
Issue: #577 «Выбор граней окон для солнечных лучей» · заход **r1** · блокирующих циклов
использовано **0/4** (лимит 4, полный трек).
Материал: ветка `issue/577-sun-ray-window-corners`, коммиты `db4b397e` (feat) и
`6f6a85a1` (fix: типизация). Материал ревью — ровно
`6f6a85a11687c4f5cd20e79a2088d05ab6be12df`, рабочая копия на этом SHA.
ТЗ прошло лёгкое... нет, **полный** трек ревью ТЗ (аналитика назвала критерий:
новый UX-контракт + новое compatibility-поле): `SPEC-REVIEW-577-r1` (жёлтый,
Medium в скоупе — недостающие release-артефакты) → `SPEC-REVIEW-577-r2`
(зелёный). Правки после зелёного ревью ТЗ в тело issue не вносились —
дельта с r2 отсутствует.
## Скоуп диффа
`git diff origin/dev...HEAD` — 74 файла, из них продуктовые (класс A):
- `src/sun.ts` — параметр `origin: SunRayOrigin` в `computeSunRays()`, сдвиг
исходного пролёта на `±d/2` по знаку `side`, вторая клипуемая грань
(«тоннель») для `outer` при `d > 0`; `SUN_RAY_ORIGINS`/`SunRayOrigin`/
`sunRayOriginOf()`.
- `src/houseplan-card.ts` — `_effSunRayOrigin()`, `origin` в ключе
`_sunRaysCache`, передача в `computeSunRays()`.
- `src/houseplan-editor-runtime.ts` — `sunRayOrigin` в `_settingsDialog`,
чтение при открытии, запись при `_saveSettingsDialog()`.
- `src/sun-settings-view.ts` (новый) — `renderSunRayOriginSelect()`.
- `src/types.ts` — `ServerConfig.settings.sun_ray_origin` плюс попутная
типизация соседних `north_deg`/`bg_mode`/`sun_rays` (эти три поля были
доступны как `any`/через `Record<string, unknown>`; типизация нужна ровно в
той же строке интерфейса, где добавляется новое поле — не отдельная работа).
- `src/i18n/{en,ru,de,fr}.json` — три ключа `gs.sun_ray_origin*` во всех
четырёх локалях, тексты совпадают с телом issue.
- `custom_components/houseplan/{validation,support_package,const}.py` —
строгий enum `vol.In(["inner","outer"])`, привилегированная фильтрация в
support package, `sun_ray_origin: "inner"` в `DEFAULT_CONFIG`.
- `scripts/config-field-registry.mjs`, `scripts/config-schema.json` —
запись реестра additive-полей и парного enum-контракта.
- `scripts/bundle-budget.mjs` — потолок `INITIAL_VIEW_GZIP_CEILING`
перецентрирован 290 900 → 291 300 (комментарий объясняет факт и причину).
- `scripts/mutation-registry.mjs` — 5 новых мутантов (ниже).
- Тесты: `test/sun.test.mjs` (+2 теста), `test/config-schema-parity.test.mjs`,
`demo/smoke_sun.mjs` (диалоговый round-trip + rekey кеша),
`tests_backend/test_{validation,support_package,ha_import_export}.py`.
- Документация (класс C, в скоупе DoD): `docs/SUN.md`,
`docs/WALL-THICKNESS.md` §5, `docs/CONFIG-COMPATIBILITY.md`,
`docs/USER-GUIDE.ru.md`, `docs/TESTING.md`, оба `docs/CHANGELOG*.md`.
- Класс D: `dist/**`, `custom_components/houseplan/frontend/**` — пересобраны
вместе с исходниками в тех же коммитах (не самостоятельный коммит).
Не найдено ни одного изменения в `demo/golden/matrix.mjs` или
`demo/golden/baselines/**` — см. находку ниже.
## Как проверялось (таблица гейтов)
Дешёвые гейты подтверждены зелёным Validate на этом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/34898363073) и
перепрогнаны здесь же по инструкции ревью (код мог быть тем же, но раз уж
песочница это позволяла — так надёжнее для нового `origin`-пути):
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | `npx tsc -p tsconfig.test.json` (для юнитов) + `npm run build` (`tsc --noEmit` внутри) | green |
| Unit — sun | `node --test test/sun.test.mjs` | 43/43 green |
| Unit — config parity | `node --test test/config-schema-parity.test.mjs` | green |
| Build + bundle sync | `npm run build && npm run bundle:sync` | green; `cmp` трёх копий (`dist`, `custom_components/.../frontend`, `demo/srv/assets`) — байт в байт |
| Bundle budget | `npm run bundle:budget` | green: initial View 290 620 Б при потолке 291 300 (комментарий кода объясняет пересчёт) |
| no-new-any | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | green: 81 добавленная строка, новых `any` нет |
| check-docs | `node scripts/check-docs.mjs` | green (diff трогает `src/**`) |
| smoke-select | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 18 прямых совпадений (полный список приложен исполнителем и здесь) |
| Смок — `smoke_sun` | `node demo/smoke_sun.mjs` | green, включая все новые `origin*`-проверки (см. ниже) |
| Смок — `smoke_sun_soft` | `node demo/smoke_sun_soft.mjs` | green — существующая геометрия/градиент/rim не задеты |
| Смок — `smoke_wall_thickness` | `node demo/smoke_wall_thickness.mjs` | green — толщина стены как подсистема не регрессировала |
| Мутация — geometry | `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test --test-name-pattern="#577: outer rays" test/sun.test.mjs` на мутанте `side = 1` | воспроизведено вручную: мутант **краснеет** (`AssertionError`, `90` → `110`), откат чист |
| Backend | не прогнан локально (нет `pytest`/`.venv-backend` в этой песочнице) | принят по зелёному job `backend` Validate на этом SHA + прочтением диффа `validation.py`/`support_package.py`/`const.py`/тестов — строки совпадают дословно с тем, что описывают тесты |
| golden:verify | не прогнан | **находка ниже** — сценария для `outer` в матрице нет вовсе, верифицировать нечего |
| performance_smoke | не прогнан (heavy-гейт, не гейт ревью; AC9 — «ревью кода») | прочтением: новая ветка — один enum в уже существующем ключе кеша и одна арифметическая ветка в существующем цикле по окнам; тайминга/сетевых вызовов не добавлено |
Остальные 17 прямых совпадений smoke-select (`smoke_bg_color`,
`smoke_color_picker_consumers`, `smoke_dialog_zombie`, `smoke_draw_wall_thickness`,
`smoke_esc_dialogs`, `smoke_grid_snap`, `smoke_gs_always`, `smoke_ha_controls`,
`smoke_help_affordance`, `smoke_room_tooltip_toggle`,
`smoke_summary_dialog_scroll/first_paint/polish/panel`,
`smoke_zigbee_topology_hover`) совпали по общим символам диалога настроек
(`_settingsDialog`, `_settings`) — не по `sun`/`computeSunRays`/`WALL`. Диффа в
их предметной области нет (ни одна строка вне sun/i18n/types их не касается),
и почитанный код `_settingsDialog` показывает только добавление одного поля
и одного независимого рендер-вызова, не меняющих существующие поля — не
прогонялись, риск регрессии этих экранов оценён как отсутствующий.
## Находки
### Medium (в скоупе, не блокирует — жёлтый вердикт)
**AC4 и AC7 объявляют доказательство `golden`, но ни один golden-сценарий и
эталон для режима `outer` в диффе не появился.**
ТЗ (план автотестов) прямо требовал: «golden matrix: отдельный
детерминированный кадр `outer` на толстой стене плюс неизменность
существующего `inner`; светлая/тёмная тема». Фактически:
- `git diff origin/dev...HEAD -- demo/golden/` — пусто. Ни одна запись в
`demo/golden/matrix.mjs` не добавлена, `demo/golden/baselines/**` не
тронут.
- `demo/smoke_sun.mjs` получил только диалоговые проверки (`#gs-sun-ray-origin`
select, save/reopen round-trip, инвалидация кеша по `_sunRaysCache`) — ни
одна из них не рендерит реальную толстую наружную стену в режиме `outer` и
не сравнивает её ни с пикселями, ни хотя бы с геометрией через
`_sunRaysCache.rays[...].polys` на сцене с реальными стенами (только
`test/sun.test.mjs` делает это на синтетической комнате).
- Хендофф-комментарий описывает только `golden:verify` на **существующих**
сценариях («относящиеся к legacy-inner солнечные сценарии... зелёные») —
это доказывает, что `inner` не изменился (AC3), но ничего не говорит о
визуальном виде `outer`.
Из трёх заявленных каналов доказательства AC4 (`unit` + `smoke` + `golden`)
фактически поставлен только один — юнит на синтетической комнате (проверено:
`node --test test/sun.test.mjs`, тест `#577: outer rays...` зелёный и
воспроизводимо падает на мутанте `side = 1`, см. таблицу гейтов). Из двух
каналов AC7 (`smoke` + `golden`) не поставлен ни один в части, специфичной
для выбора грани — существующие смоки на тему, свет/тьма, киоск проверяют
общий рендер лучей, но никогда не переключают `sun_ray_origin` при этом.
Почему это не просто формальность: новый код в `computeSunRays()` при
`origin === 'outer' && d > 0` кладёт в `SunRay.polys` **два отдельных
полигона** (клип по тоннелю плюс клип по комнатному контуру), рисуемых как
два разных `<polygon>` с одной и той же полупрозрачной SVG-градиентной
заливкой и общей границей на внутренней грани стены. Совпадение координат
этой общей границы проверено юнитом только на синтетической фикстуре, где
`innerByRoom` подставлен вручную равным той же формуле `w.x + normal·d/2`,
которой независимо (через `_innerRoomContour`/`innerContourForRoom`) в
реальном рендере пользуется общий контур комнаты. Пара чисел «край тоннеля»
и «край комнатного контура» получены двумя разными путями в проде (это,
впрочем, тот же самый пре-существующий контракт, на который уже опирается
сегодняшний `inner`, так что риск разъезда не новый — но эффект нового
двойного полигона на стыке — растровый шов при полупрозрачной заливке —
проверяется только скриншотом, и скриншота нет). Это ровно тот исполнимый
oracle, который ТЗ и назвало, и без него утверждение AC4 «свет не течёт по
телу стены снаружи окна» подтверждено только на бумаге у стены, которую
тест сам же построил без реальной сшивки геометрии.
**Правка тривиальна и не блокирует:** без High-находок вердикт жёлтый,
доработка — в этой же задаче (#202). Ожидаемое исправление: одна запись в
`demo/golden/matrix.mjs` (толстая наружная стена, `settings.sun_ray_origin:
'outer'`, возможно светлая+тёмная тема как в существующей паре
`day-cycle-*`), плюс `demo/smoke_sun.mjs`-проверка, что при `outer` за
пределами `[innerX, outerX]` по нормали свет не выходит на реальной сцене
(не только в юните). Приёмка нового эталона — как обычно, только через
`npm run golden:accept -- --reviewed` на полном Linux-артефакте; до этого
момента ревью не может подтвердить AC4/AC7 golden-каналом и открыто говорит
об этом здесь, а не молчит.
### Low
Нет.
## Что проверено и корректно
- **AC1 (UI/сохранение):** select `#gs-sun-ray-origin` рендерится ровно один
раз, сразу после переключателя «Солнце в окнах» (`_settingsDialog`),
никогда не `disabled`, независимо от состояния лучей — подтверждено и
чтением (`sun-settings-view.ts` не содержит атрибута `disabled`), и смоком
(`originAvailableWhenRaysOff: true`). Настройка пространства нового
контрола не получила — `grep` не находит `sunRayOrigin`/`sun_ray_origin` ни
в одном коде space-диалога.
- **AC2 (совместимость):** `sunRayOriginOf()` типизирован на
`{ sun_ray_origin?: unknown } | null | undefined` и фейл-safe на `inner`
для `null/''/0/true/[]/{}` — юнит-тест `#577: zero-depth walls are
identical...` перечисляет все эти случаи явно. Backend:
`vol.In(["inner","outer"])`, тест перечисляет 7 негодных значений (включая
`None`, `""`, `0`, `True`, `[]`, `{}`) и ждёт `vol.Invalid` на каждом —
протестировано мутантом `_SUN_RAY_ORIGIN = str`, который зарегистрирован
как отдельный обязательный свидетель в `mutation-registry.mjs`. Экспорт/
импорт и support package — юнит-тесты `test_ha_import_export.py` /
`test_support_package.py` проверяют round-trip и фильтрацию
неизвестной строки соответственно.
- **AC3 (внутренняя грань):** формула `sourceX = w.x + normal·d·side/2` с
`side=1` для `inner` — байт-в-байт та же, что была в `dev` до этой задачи
(сверено построчно с `git show origin/dev:src/sun.ts`); существующие golden
для `inner` (`day-cycle-*`, `lighting-sun-window-state-only-dark`) остаются
зелёными по заявлению автора — независимая проверка не требуется, так как
сама формула не менялась.
- **AC5 (инварианты луча):** юнит `#577: outer rays start at the exterior
corners...` явно сравнивает `len`, `dir`, `normal`, `depth` между `inner` и
`outer` — все равны; нулевая толщина даёт `deepEqual(outer, inner)` —
проверено самим тестом и повторно прогнано здесь.
- **AC6 (кеш):** `origin` — часть `_sunRaysCache` ключа
(`houseplan-card.ts:10505`); мутант, убирающий `origin` из шаблонной
строки, ловится смоком `smoke_sun` (зарегистрирован, воспроизводимость не
перепроверялась вручную — дорогой браузерный гейт, довод §2.7 «мутант
обязателен... там ревьюер не воспроизведёт отрицательный прогон второй
раз», сам смок green на живом коде подтверждён).
- **AC8 (соседние контракты):** `windowWallInfo`, `isExteriorWall`,
per-space `sun_rays`/`north_deg` (`sunRaysOn`/`northDegOf`) не тронуты
диффом вообще (0 строк в диффе); `smoke_sun`, `smoke_sun_soft`,
`smoke_wall_thickness` green подтверждают отсутствие регрессии на
прилегающих сценариях (rain/clouds, editors-off, thin/thick wall
hatching).
- **AC9 (производительность):** прочтением — новый код добавляет один
enum-literal в уже вычисляемый ключ мемоизации и одну ветку `side = ... ?
-1 : 1` внутри существующего цикла по окнам; ни новых таймеров, ни сетевых
вызовов, ни работы вне пересчёта по (`azimuth, elevation, north, origin,
_cfgEpoch, ...`). Проверено чтением, не исполнением (performance_smoke —
heavy-гейт вне гейта ревью).
- **i18n:** все четыре локали (`en/ru/de/fr`) получили одинаковый набор из
трёх ключей; русские тексты дословно совпадают с телом issue.
- **Config registry / schema parity:** `settings.sun_ray_origin` зарегистрирован
в `scripts/config-field-registry.mjs` по образцу `bg_mode`/`summary_panel`;
`test/config-schema-parity.test.mjs` добавляет пару
`SUN_RAY_ORIGINS`↔`config.settings.sun_ray_origin` и прогнан зелёным.
- **Откат:** удаление поля возвращает `inner` — подтверждено самой формулой
`sunRayOriginOf()` (fail-closed на `inner`), отдельного теста на «поле
физически удалено из json» не требуется — это тот же путь, что и
«отсутствует».
- **Бандл/бюджет:** три копии бандла (`dist`, `custom_components/.../frontend`,
`demo/srv/assets`) идентичны байт-в-байт после `npm run bundle:sync`;
`INITIAL_VIEW_GZIP_CEILING` поднят с обоснованием в комментарии кода и
подтверждён замером (290 620 Б).
- **Типизация `types.ts`:** добавление `north_deg`/`bg_mode`/`sun_rays` рядом
с новым `sun_ray_origin` — не самостоятельная незапрошенная работа: это та
же строка интерфейса, что и новое поле, и снимает ранее нетипизированный
доступ через `Record<string, unknown>` (`houseplan-editor-runtime.ts:10022`);
не расширяет скоуп задачи.
## Чего не проверял и почему
- **`python -m pytest tests_backend`** — не прогонялся: в этой песочнице нет
`pytest`/`.venv-backend`. Принято по зелёному job `backend` Validate на
точном SHA `6f6a85a1` плюс построчному сличению изменённых файлов с тем,
что тесты утверждают (см. таблицу гейтов) — это чтение, не исполнение,
зафиксировано явно.
- **`npm run golden:verify` / `golden:capture`** — не прогонялся мной: без
нового сценария для `outer` в матрице результат ничего нового не покажет
(то же самое, что зафиксировано находкой выше); прогон существующей
матрицы повторял бы то, что уже подтвердил автор на Linux CI, и не
является Windows/локально принимаемым эталоном в любом случае.
- **`performance_smoke` (glow/interaction профили)** — heavy-гейт, не входит
в обязательный набор код-ревью; AC9 сам называет проверкой «ревью кода» —
выполнено чтением (см. выше).
- **Полный `ls demo/smoke_*.mjs` (249 смоков)** — прогонялись только 3 из 18
прямых совпадений (`smoke_sun`, `smoke_sun_soft`, `smoke_wall_thickness`) —
выбраны как единственные, реально пересекающиеся с изменённым кодом
(`computeSunRays`, `WALL`); остальные 15 совпали только по общему символу
диалога настроек, не по предметной области — не прогонялись, обоснование
выше.
- **`npm run invariants`** — диффа не в модели геометрии (рёбра комнат,
`layout`, `marker.space`, записи толщины) нет, только в рендере солнечных
лучей и i18n/настройках; гейт не относится к этому диффу, поэтому не
прогонялся (автор прогнал его добровольно на своём фиксture — это
избыточно, но не вредно).
## Материал раунда
- Ветка: `issue/577-sun-ray-window-corners`
- SHA материала: `6f6a85a11687c4f5cd20e79a2088d05ab6be12df`
- Дерево: `2e07221ac58df96dcd816dd7cfa9df614addf09d`
- Предыдущий раунд: нет (это r1 код-ревью; ревью ТЗ — отдельная цепочка,
`SPEC-REVIEW-577-r1`/`-r2`, закрыта зелёным до начала реализации)
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/577-sun-ray-window-corners`, коммит `6f6a85a11687` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `2e07221ac58df96dcd816dd7cfa9df614addf09d`
```
git log --all --format='%H %T' | grep 2e07221ac58d
```
- Тело issue: `ebb1c05540c7191db0b4a76841f19b1371239caa0d41798ba82ec319116b32fc`
- Вердикт конвейера: `yellow` · High 0