Files
houseplan-card/docs/reviews/SPEC-REVIEW-580-r1.md
T
2026-09-15 09:05:58 +03:00

224 lines
20 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.
# SPEC-REVIEW-580-r1
**Issue:** #580 «Солнечные лучи от внешних углов проходят сквозь толстые стены»
**Этап:** ТЗ на ревью (`S4-spec-review`), трек — полный продуктовый
**Заход:** r1 · блокирующих циклов израсходовано 0/4 (лимит для полного трека — 4)
**Материал:** тело issue #580, раздел `## ТЗ`, на момент публикации комментария
«Передача ТЗ на ревью» (2026-09-15T04:52:39Z), ветка `issue/580-outer-sun-wall-occlusion`
от `d37b2c9f8a4369082fec6970117cc97dd1e303c7` без продуктовых изменений.
## Скоуп
Задача чинит регрессию в геометрии солнечных лучей режима `sun_ray_origin: outer`
(#577): на толстой наружной стене с окном при косом положении солнца
`computeSunRays()` строит комнатную часть луча пересечением ПОЛНОГО внешнего
пролёта окна с контуром комнаты, а не с интервалом, реально прошедшим сквозь
оконный туннель — откосы стены не отсекают лишнюю ширину, и часть заливки
визуально идёт сквозь тело стены. Правка ограничена `src/sun.ts` и его тестами;
UI, i18n, схема конфигурации и touch-контракт не затрагиваются согласно самому
ТЗ.
Job по `docs/SCOPE.md`: J1/J5 (свет как часть спатиального обзора) не при
чём напрямую — window rays это отдельный decorative-визуал, но исправление
физически неверного визуала — прямая работа над качеством уже принятой фичи
#577, а не новая функциональность вне списка задач. Отклонений от SCOPE.md не
найдено.
## Как проверялось
1. Прочитан `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (§1–§10, до строки 871
первой порции + смежные разделы §2.4/§2.5/§2.10/§7.1/§7.2).
2. Прочитано тело issue #580 (шапка бага + `## ТЗ`) и оба комментария
(«Аналитика», «Передача ТЗ на ревью») через `gh issue view --json`.
3. Прочитан канонический документ подсистемы `docs/SUN.md` целиком, включая
раздел «Window light wedges — `settings.sun_ray_origin`» и список файлов.
4. Прочитан раздел «Лучи солнца» `docs/USER-GUIDE.ru.md` (строки ~1634–1665):
таблица уже содержит строку «Толстая внешняя стена, режим «внешние углы» —
Луч начинается с фасадной стороны и проходит к комнате только внутри
оконного тоннеля» — то есть **пользовательский текст уже описывает
исправленное поведение**, реализация ему пока не соответствует.
5. Прочитан код `computeSunRays()` в `src/sun.ts` (строки 402–459) построчно,
чтобы проверить, что диагноз ТЗ («второе пересечение снова допускает в
комнату всю исходную ширину луча») соответствует фактическому коду, а не
догадке: `polys = clipToRoom(quad, clipPoly)` действительно клипует ПОЛНЫЙ
`quad` (от внешнего пролёта) напрямую по контуру комнаты, а туннельная
часть добавляется ОТДЕЛЬНЫМ независимым клипом (`if (origin === 'outer' &&
d > 0) { … polys.push(...) }`) — комнатная и туннельная части не связаны
пересечением друг с другом. Диагноз подтверждён чтением, не исполнением.
6. Прочитан существующий тест `#577` в `test/sun.test.mjs:468` — он проверяет
`outer`-режим при азимуте 270°/угле окна 90° (перпендикулярное падение),
поэтому не мог поймать именно эту регрессию на косом солнце. Технический
подход ТЗ (разделить туннельную и комнатную часть, для комнатной считать
траектории, прошедшие оба пролёта) реализуем поверх уже существующих
входов функции (`wallDepthByOpening`, координаты окна, контур комнаты) —
новых геометрических данных не требует.
7. Подтверждено существование `demo/smoke_sun.mjs` и `demo/golden/matrix.mjs`,
на которые ссылается план тестов AC4/AC5.
Ручного тестирования UI не проводилось — этап ТЗ, кода ещё нет
(`issue/580-outer-sun-wall-occlusion` без продуктовых изменений).
## Находки
### Medium (в скоупе задачи — правится в том же ТЗ)
**M1. ТЗ не отвечает на несколько обязательных пунктов §7.1 / чек-листа DoR
§2.5, оставляя их фактически открытыми, хотя ответ на каждый тривиален.**
Отсутствуют как явные утверждения:
- **Сценарий** (какая персона/поверхность/момент, `docs/SCOPE.md`) и **что
человек увидит до и после** одной фразой без терминов реализации — первые
два обязательных раздела ТЗ идут первыми не случайно (`PROCESS.md` §7.1).
Раздел `## ТЗ` начинается сразу с «Проблема» в терминах реализации
(`sun_ray_origin: outer`, «откосы», «внутренняя плоскость»). Материал для
этих двух разделов есть в шапке issue («Ожидаемое поведение»), но она тоже
сформулирована в терминах реализации и не перенесена в ТЗ как отдельный
продуктовый раздел.
- **i18n** — DoR требует «ключи en + ru перечислены»; в ТЗ нет даже строки
«нет новых ключей», хотя ответ очевиден.
- **Влияние на touch** (`docs/TOUCH-SUPPORT.md`, View и киоск — блокирующие
пункты DoR) — не упомянуто, хотя window rays рендерятся именно в View/
киоске, для которых touch-совместимость обязательна к проверке по чек-листу.
- **Влияние на производительность** — не упомянуто, хотя правка меняет
горячий путь `computeSunRays()` (пересчёт при каждом изменении
азимута/элевации, см. `docs/SUN.md`: «Sun geometry is recomputed ONLY when
… change» — то есть путь всё равно активный и не редкий) добавлением
дополнительного пересечения полигонов. DoR требует «названо (или явно
«нет»)» — сейчас не названо никак.
- **Риски** как раздел самого ТЗ — риски перечислены только в отдельном
комментарии «Аналитика» (сложность 5/10, риск 6/10, «важно не изменить
режим внутренних углов, прямое падение, нулевую толщину и fade/rim»), но не
перенесены в тело `## ТЗ`. Аналитика и ТЗ — разные артефакты процесса
(§2.2 vs §2.3); DoR (§2.5) требует риски именно при переходе в «Готово к
разработке», то есть от ТЗ, а не от предшествующего комментария аналитика.
Почему это Medium, а не Low: без явных ответов на эти пункты DoR-чек-лист
§2.5 формально не проходится ни по одному из пяти перечисленных пунктов —
не потому, что ответы сложны (все пять тривиальны — «нет» или короткая
фраза), а потому что их сейчас в ТЗ просто нет, и переход в `S5-ready`
по букве процесса не может считаться завершённым. Правка дешёвая: добавить
пять коротких строк/раздел, не переписывая контракт и AC.
Как закрыть: добавить в тело `## ТЗ` явные строки: сценарий (персона,
поверхность View/kiosk, момент — низкое косое солнце), одну фразу «что
человек увидит» без терминов реализации, `i18n: нет новых ключей`, `touch:
не меняется (View/kiosk рендерят как раньше, взаимодействия нет)`,
`производительность: доп. пересечение полигона в уже пересчитываемом на
каждое изменение азимута/элевации пути, без нового порядка сложности —
либо явную оценку, если авторы считают иначе`, и короткий раздел «Риски»
(перенести формулировку из комментария аналитика).
### Low (на усмотрение автора — можно поправить или снять с записью)
**L1. Контракт п.4 использует немодальную формулировку («может оставаться
видимым»), которую ни один AC не проверяет.**
> «Если направление настолько косое, что ни один луч не проходит через оба
> пролёта, комнатная часть отсутствует; свет внутри доступной части туннеля
> **может** оставаться видимым.»
AC2 проверяет только пустоту комнатной части в этом случае
(«при предельно косом направлении без сквозной видимости комнатная часть
пуста»), но не утверждает и не проверяет, остаётся ли туннельная часть
освещённой. Формулировка «может» уместна как честное признание
неопределённости (поведение туннельной части не входит в эту правку и,
по всей видимости, не меняется относительно уже принятого #577
поведения), но в разделе «Контракт», который по определению должен
описывать однозначное поведение, такая модальность читается как
недорешённый пункт. Не блокирует: последствия ограничены тем самым
пограничным случаем, который вне зависимости от трактовки не влияет на
AC1–AC3 (типичные и предельные-но-не-нулевые углы) и не расширяет скоуп.
Рекомендация: либо явно пометить как «поведение вне контракта данной задачи,
не меняется» (аналогично формулировке п.6 про взаимное затенение), либо
удалить хеджирующее слово и добавить проверку в AC4 (там уже есть
контрольная точка «за закрытым откосом»).
## Что проверено и корректно
- **Диагноз бага соответствует коду**, а не является догадкой: строки
423/440–450 `src/sun.ts` подтверждают, что комнатная и туннельная части
луча в режиме `outer` считаются двумя независимыми клипами одного и того
же полного `quad`, без пересечения друг с другом — именно это описывает
«Причина» в комментарии «Аналитика» и «Проблема» в ТЗ.
- **Технический подход реализуем на существующих входных данных**
(`wallDepthByOpening`, координаты/угол/длина окна, контур комнаты) — не
требует новой геометрии стен (откосов, рам) сверх уже используемой глубины
`d`. Заявление «принято предположительно» про отсутствие моделирования
скошенных откосов и отдельной глубины рамы согласуется с тем, что этой
геометрии в проекте вообще нет ни для одного другого сценария (глубина
считается прямоугольным туннелем и в текущем, не только багованном, коде).
- **Регрессионный периметр назван и обоснован верно**: контракт п.3 (нет шва
между туннелем и комнатной частью, полная ширина при перпендикулярном
падении), п.5 (`inner`, нулевая толщина, направление/длина/fade/rim не
меняются) точно называют места, где чинить проще всего сломать — они прямо
соответствуют существующим тестам `#577` (`test/sun.test.mjs:468,495`) и
`DEV-EB173-01` (`test/sun.test.mjs:509`), которые эта задача не должна
регрессировать.
- **`docs/USER-GUIDE.ru.md` не нуждается в правке** — таблица «Поведение» уже
описывает целевое (исправленное), а не текущее ошибочное поведение
режима «внешние углы» (строка «...проходит к комнате только внутри
оконного тоннеля»). Хорошо, что автор не стал ничего менять в
пользовательской документации, поскольку менять там нечего; отдельно
отмечаю здесь как проверенный факт, а не находку.
- **AC1–AC7 однозначны и каждому указан способ доказательства** (`unit` ×3,
`browser smoke`, `golden`, гейты, `review`) — ни один AC не описывает
реализацию вместо наблюдаемого результата (AC1/AC2/AC4/AC5 говорят о
видимых/измеримых геометрических свойствах луча, AC3 — о регрессионной
идентичности, AC6/AC7 — гейты и объём диффа).
- **«Принятые предположения» оформлены по правилу §7.1** явным блоком, а не
вписаны в контракт как факт — ревьюер может их оспорить, оба технические
(не продуктовые), эскалация владельцу не требуется.
- **Не-скоуп назван явно и совпадает с ранее принятым лимитом** SUN.md
(«mutual shading… NOT computed»): контракт п.6 не пытается расширить
задачу на взаимное затенение стен/крыльев.
- Track-решение («полный трек», не `small`) обосновано названным критерием
(сложность/риск выше 3, меняется геометрический алгоритм) — соответствует
требованию §2.2 называть нарушенный критерий, а не писать «обычный трек»
без обоснования.
## Чего не проверял
- Не проверял `npx tsc --noEmit` / `npm test` / `npm run build` — на этапе
ТЗ продуктового кода ещё нет (ветка `issue/580-outer-sun-wall-occlusion`
без изменений от `d37b2c9f`), гонять гейты не над чем.
- Не выполнял браузерные смоки и golden — они станут актуальны на код-ревью,
когда появится реализация AC1–AC5.
- Не проверял `python -m pytest tests_backend` — задача не касается
`custom_components/**/*.py`.
- Не пересчитывал производительность численно (нет кода) — отметил как
находку M1 отсутствие даже качественной оценки в тексте ТЗ.
## Вердикт
Жёлтый: High-находок нет, но найден Medium в скоупе задачи (M1) — несколько
обязательных пунктов чек-листа §7.1/DoR §2.5 не имеют явного ответа в теле
`## ТЗ`, хотя ответ на каждый тривиален и не меняет контракт/AC. Технический
разбор задачи (диагноз, подход, критерии приёмки) проверен по коду и
корректен; возврат нужен только чтобы явно закрыть пропущенные разделы, а не
чтобы переписывать решение.
## Материал раунда
- Issue: #580, репозиторий Matysh/houseplan-card.
- Материал ТЗ: тело issue, раздел `## ТЗ`, на момент комментария
`IC_kwDOTOcLQM8AAAABUkBntg` (2026-09-15T04:52:39Z, «Передача ТЗ на ревью»).
- Ветка `issue/580-outer-sun-wall-occlusion`, `HEAD = d37b2c9f8a4369082fec6970117cc97dd1e303c7`
(без продуктовых изменений на момент ревью).
- Заход r1, документ первый; предыдущих раундов нет — разделы «Закрытие
раунда r0» и «Унаследовано из r0» не применимы.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/580-outer-sun-wall-occlusion`, коммит `d37b2c9f8a43` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `cd142c70cdb0ab25b12dd4fa2a77c9d955f01940`
```
git log --all --format='%H %T' | grep cd142c70cdb0
```
- Тело issue: `25c7fa5fa2430ab0ce56a0f7684ab0fb192b932a209aba248b951ddd82189135`
- Вердикт конвейера: `yellow` · High 0