# CODE-REVIEW-132-r2 - **Issue:** https://github.com/Matysh/houseplan-card/issues/132 - **Связанный bug в том же scope:** #185 (закрывается тем же кодом, по решению владельца) - **Диапазон:** `origin/dev...HEAD` на ветке `issue/132-partition-openings-v2`, но предметно этот цикл разбирает только новый коммит с момента r1 — `3fe0f8c fix: address partition opening review regressions` (16 файлов, +306/−244), поверх уже принятого без изменений `9f77e3e` (r1 покрыл его целиком). - **ТЗ:** `docs/specs/132-partition-openings.md`, зелёное ревью `docs/reviews/SPEC-REVIEW-132-r1.md` - **Предыдущий цикл:** `docs/reviews/CODE-REVIEW-132-r1.md` — красный, High: 1 (`build_space_merge()` не ремапил `opening.host.id`), Medium: 3 (#186, #187, #188, заведены отдельно, не входят в этот фикс) - **Роль:** ревьюер кода (не автор), этап `S7-code-review` - **Цикл:** r2/4 ## Скоуп ревью Коммит `3fe0f8c` заявлен как исправление ровно High-1 из r1. Фактически несёт два независимых изменения: 1. **Ремап `opening.host.id`** в `build_space_merge()` (`custom_components/houseplan/import_export.py:845-854`) — прямое исправление High-1, плюс регрессионный тест в `tests_backend/test_ha_import_export.py`. 2. **Разделение presentation/structural снапшотов** `buildPlanSnapGeometry()` в `src/houseplan-card.ts` (новый `_planStructuralGeometrySnapshot()`, `_planSnapGeometrySnapshot()` восстанавливает opening-cuts, `_wallGraphSources` переключён на структурный снапшот) плюс правки `src/plan-snap-overlay.ts` (только докстринг), `docs/CANVAS.md`, changelog RU/EN, doc-screenshots. Второе не было прямо потребовано r1 (High-1 касался только backend), но устраняет регрессию, которую r1 обнаружил и не смог объяснить (golden-diff `plan-snap-line-gaps-dark`, зафиксирован в r1 как «Чего не проверял», не как находка) — это восстановление поведения `_planSnapOpeningCuts()`, которое существовало на `origin/dev` до правки #132 и было потеряно в `9f77e3e`. Не считаю это расширением скоупа: правка находится в файлах и подсистеме, которые сам r1 разбирал по AC5/AC6, и не добавляет пользователю ничего вне контракта из ТЗ §11. Прочие 11 AC (AC1–AC4, AC6–AC12) не затронуты диапазоном `3fe0f8c` — сам файл-состав диффа (backend id-ремап + presentation/structural split) не пересекается с их кодовыми путями placement/lifecycle/light/HA-state/compatibility-schema/i18n, что подтверждено построчным чтением диффа. Повторный разбор по каждому из них не производился — r1 их разобрал, вердикт по этому циклу ограничен тем, что изменилось. ## Как проверялось Дешёвые гейты (прогнаны лично в этой сессии, точные команды и результат): | Гейт | Команда | Результат | |---|---|---| | Typecheck | `npx tsc --noEmit` | зелёный, без вывода | | Unit | `npm test` | `870 passed / 0 failed`, совпадает с заявленным | | Build + сверка бандлов | `npm run build && cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js && cmp dist/houseplan-card.js demo/srv/assets/houseplan-card.js` | сборка ок, обе копии побайтово совпадают, `git status --short` после сборки пуст (закоммиченный бандл идентичен свежей сборке) | Смоки, прогнанные локально (по diff — только затронутые этим коммитом поверхности): | Смок | Результат | Почему выбран | |---|---|---| | `node demo/smoke_partition_openings.mjs` | OK, все 12 полей `true` | использует переименованный `_planStructuralGeometrySnapshot`, регрессия по AC1/AC3/AC5 | | `node demo/smoke_room_autoclose.mjs` | OK, все 9 полей `true` | `openingKeepsStructuralAutoClose: true` — #185 не пострадал от разделения снапшотов | | `node demo/smoke_plan_snap_overlay.mjs` | OK, все 32 поля `true`, включая `openingGapHasNoLine: true` | прямая проверка presentation-cut; **не входил** в список из 12 смоков r1, хотя diff трогает именно эту поверхность — восполняю здесь | Остальные смоки из целевого набора r1 (`smoke_opening_preview`, `smoke_glow`, `smoke_inert_openings`, `smoke_opening_binding`, `smoke_opening_tunnel_fill`, `smoke_wall_junctions`, `smoke_unified_wall_tool`, `smoke_isometric_contract`, `smoke_openwall`, `smoke_sun`) не перегонялись: `3fe0f8c` не меняет код на их путях (placement/light/HA-binding/junction geometry/iso/#173 regression — подтверждено чтением диффа), а r1 уже доказал их зелёными на предшествующем коммите. Не весь набор из 141 — избыточно для 16-файлового фикса. **Backend.** В окружении этого ревью нет `homeassistant`/`.venv-backend` (то же ограничение, что у r1). Установил `pytest`+`voluptuous` и прогнал чистое подмножество: `python -m pytest tests_backend -q` → **139 passed** — тот же результат, что у r1, `test_ha_import_export.py` (файл с фиксом High-1 и новым регрессионным тестом) в этом прогоне **не участвовал** (`conftest.py` молча игнорирует `test_ha_*.py` без `homeassistant`). Вместо предположения проверил через реальный CI на этом SHA (Linux, `homeassistant` установлен): job `backend` для `3fe0f8c` — **`completed / success`**, лог оканчивается `280 passed in 4.30s`, команда в логе — `python -m pytest tests_backend/ -q`. Для контраста: тот же job на предыдущем коммите `9f77e3e` (до фикса) — `completed / failure`. Это прямое подтверждение, что регрессионный тест `test_space_merge_remaps_every_space_owned_id_and_room_link` (теперь с `host`-фикстурой) действительно запускается на Linux CI и проходит после фикса, а до фикса — ломался. Не «verified» без команды: команда и результат процитированы из фактического лога джобы (`https://github.com/Matysh/houseplan-card/actions/runs/32193375736/job/95892362527`). **Golden.** `job golden` на `3fe0f8c` — `completed / success`, в логе все перечисленные сцены `passed`, включая ранее «different»/нерасследованную в r1 `plan-snap-line-gaps-dark` — теперь **`passed`**. Это лучше, чем 60/62 у r1: второй компонент фикса (presentation-снапшот) действительно устранил тот golden-diff, который r1 оставил неразобранным. **Прочитано построчно, не исполнено:** - `custom_components/houseplan/import_export.py:845-854` — ремап `opening.host.id` через тот же `id_map`, что и id перегородки; порядок операций корректен: цикл ремапа id (`:830-838`) выполняется раньше, `id_map` уже содержит `old_partition_id → new_partition_id`, когда цикл над `openings` (`:849-854`) читает `host.id`. - `src/houseplan-card.ts:6156-6198` — `_planSnapGeometrySnapshot` восстановил `roomCuts: [...openCuts, ...this._planSnapOpeningCuts(space, openCuts)]` (буквально то же выражение, что было на `origin/dev` до `9f77e3e`, см. `git show origin/dev:src/houseplan-card.ts` — метод `_planSnapOpeningCuts` существовал там же). Новый `_planStructuralGeometrySnapshot` использует `roomCuts: this._openCuts()` — ровно то, что `_planSnapGeometrySnapshot` вычислял в промежуточном (r1) коде. Все потребители разделены корректно и без остатка: `_wallGraphSources` (`:6900-6901`, единственный вызывающий `_wallFaceGraph`/#185-путь) — на структурный; четыре презентационных сайта (`:6213, 6236, 11771, 17598, 17688`) — на исходный. Grep по всему файлу подтверждает отсутствие смешанных вызовов. ## Находки ### High Нет. High-1 из r1 исправлен корректно (см. «Как проверялось» — фикс, тест и зелёный CI). ### Medium (заведена отдельным issue) #### Medium-1 → #189 — presentation snap-overlay не режет ось независимой перегородки в месте её собственного проёма Второй компонент этого коммита восстанавливает presentation-cut для **legacy room-wall** openings и для **composite** partition/room-wall случая, но не для основного сценария #132 — проёма на независимой перегородке, не совпадающей ни с какой стеной комнаты. **Почему это не тривиальная догадка, а воспроизведённый дефект.** Прочитано и эмпирически проверено (headless Chromium, `demo/serve.mjs`, тот же харнесс, что у смоков): пространство с одной перегородкой `{a:[0.25,0.5], b:[0.75,0.5]}` без комнат и дверью `host:{kind:'partition', id, t:0.5}` даёт `card._planSnapGeometrySnapshot().value.segments` **один** сегмент `partition` от `[250,500]` до `[750,500]` — без разрыва в точке проёма (x=500). Причина прослеживается по коду: - `_roomWallOpeningInputs()` (`src/houseplan-card.ts:7834-7855`) для partition-hosted проёма возвращает вход в выборку **только** когда `partitionOpeningHasCompositeRoomWall(...)` истинно — т.е. только для composite-случая (решение по Q4); - `_planSnapOpeningCuts()` (`:6141-6154`) строит cuts исключительно из этой выборки — для обычного (не composite) partition-hosted проёма cut не создаётся вообще; - `buildPlanSnapGeometry()` (`src/plan-snap-overlay.ts:118-156`) в любом случае передаёt `cuts: []` для `kind: 'partition'` безусловно (`roomCuts` применяется только к `kind: 'room'`, `:123-131`) — так что даже если бы cut был вычислен, для partition-источника он никуда не попал бы без изменения этой функции. **Почему это противоречит контракту, а не просто пробел evidence.** ТЗ §11 буквально: «Контракт применяется одинаково к legacy room-wall opening и новому partition-hosted opening» — про presentation/snap boundary cut. Этот же коммит переписал `docs/CANVAS.md` («Door, window, gate and intentionally open-span intervals are cut from presentation axes», без оговорки про host kind) и оба changelog («the editor's visual snap guide keeps its physical gap across the opening» / «визуальная направляющая привязки по-прежнему показывает физический разрыв в месте проёма») — универсально, без оговорки о composite-случае. Заявленное в документации и реализованное в коде расходятся именно для главного, а не краевого сценария #132 (проём на независимой, не совпадающей с комнатой перегородке). **Почему Medium, не High.** Не портит сохранённые данные, не ломает physical/light геометрию (та режется верно — `hostBodyHasFullDepthOpeningGap` в `smoke_partition_openings.mjs` зелёный) и не ломает structural room-face граф (#185 — `_planStructuralGeometrySnapshot` намеренно и корректно игнорирует cuts для обоих host kind, что и требуется). Затрагивает только inline snap-подсказку инструмента «Стены» в Plan-редакторе (admin-only поверхность): пользователь может получить снап на точку, физически лежащую внутри проёма, как если бы там была сплошная кладка — ровно то поведение, которое контракт «opening gap remains a gap» (#173) должен исключать, только для нового host kind. Не покрыто ни одним из 12 AC ТЗ буквально (все они описывают geometry/light/HA/render — не snap-guide), поэтому не проваливает формальный AC, но нарушает явный текст §11 и текст только что обновлённой документации/changelog в этом же коммите. Заведено: #189 (bug, P2, S1-new), со ссылкой на #132 и точной репродукцией. ### Low Нет новых Low в этом цикле. Три Low из SPEC-REVIEW-132-r1 были закрыты до начала кода (запись автора «Начало реализации»); не пересматривались здесь, диапазон `3fe0f8c` их не касается. ## AC — статус после r2 AC1–AC4, AC6–AC8, AC10–AC12: без изменений относительно r1 (**пройдены**, диапазон `3fe0f8c` их кода не касается — подтверждено чтением diff-состава). AC5 (#185 room closure): **пройден**, подтверждено заново для этого коммита (`smoke_room_autoclose.mjs`: `openingKeepsStructuralAutoClose: true`; `smoke_partition_openings.mjs`: `openingKeepsRoomFaceAxisContinuous: true` через переименованный, но не изменённый по семантике метод). Разделение снапшотов не меняет вход `_wallFaceGraph` — структурный снапшот вычисляется идентично тому, что использовался в r1. AC9 (compatibility): **пройден** — это ровно то, что чинил High-1. Backend export/import/duplicate одного space с partition-hosted openings теперь сохраняет referential integrity `host.id`; подтверждено новым регрессионным кейсом в `test_ha_import_export.py` и зелёным `backend` job на точном SHA `3fe0f8c` в Linux CI (280 passed, 0 failed). Ни один из 12 AC не описывает presentation snap-overlay буквально (см. Medium-1/#189) — находка не проваливает формальный AC, но является нарушением §11 ТЗ вне списка AC1–AC12. ## Что проверено и корректно - High-1 из r1 фактически исправлен: код читается корректно (порядок ремапа, тот же `id_map`, что у остальных ссылок), новый regression-тест целенаправленно бьёт по сценарию High-1 (фикстура с `host`), Linux CI backend job зелёный на точном SHA с командой и результатом в логе. - Presentation/structural разделение снапшотов реализовано чисто: ни одного оставшегося смешанного вызова (`_wallGraphSources` — единственный потребитель структурного, четыре презентационных сайта — исходного; проверено grep по всему файлу). - Golden CI зелёный полностью (все перечисленные сцены `passed`, включая ранее неразобранную `plan-snap-line-gaps-dark`), лучше результата r1. - Три Medium из r1 (#186, #187, #188) корректно остаются отдельными открытыми issue, не включены и не спрятаны в этом фиксе — проверено `gh issue view` по каждому: все три открыты, `S1-new`, ссылаются на #132. - Трейлеры коммита `3fe0f8c`: `Issue: #132`, `User-Visible: yes`, оба changelog (RU+EN) правлены в этом же коммите — проверено `git show --stat`. - Bundle freshness: три копии (`dist/`, `custom_components/houseplan/frontend/`, `demo/srv/assets/`) побайтово идентичны после чистой пересборки в этой сессии; `git status --short` после сборки пуст. ## Чего не проверял - Полный набор из 141 браузерного смока — не запускал, diff 16-файлового фикса не касается большинства поверхностей (обоснование выбора трёх целевых смоков — выше). CI job `smoke` на момент завершения этого документа ещё выполнялся (`in_progress`) — полный прогон относится к предрелизному гейту, не к гейту этого код-ревью, и не блокирует вердикт. - `tests_backend/test_ha_import_export.py` не выполнялся мной локально (нет `homeassistant` в этом окружении) — заменено проверкой факта и результата прогона на Linux CI на точном SHA (см. «Как проверялось»), а не предположением. - `performance_smoke` — не запускал, diff не касается кеш-инвалидации/hot-path (только backend id-ремап и разделение уже кешируемых снапшотов по тому же шаблону, что был). - Composite (coincident partition + room-wall) presentation-cut сценарий — не проверял отдельно эмпирически в этом цикле; логика `_roomWallOpeningInputs` не менялась в `3fe0f8c` и была прочитана r1 как корректная для этого случая (см. CODE-REVIEW-132-r1, разбор AC2). Мой репродукшн для #189 намеренно взял **не**-composite случай, чтобы изолировать дефект. - Точную причину, почему исходный golden-diff `plan-snap-line-gaps-dark` использовал именно room-wall, а не partition-сценарий (и поэтому не вскрыл #189 в r1/r2 через golden) — не расследовал; вне golden-набора нет сцены с независимой перегородкой и проёмом без совпадения с комнатой, что и объясняет, почему CI не поймал #189. ## Вердикт Зелёный · цикл r2/4 · High: 0 · Medium: 1 → #189 High-1 из r1 исправлен и подтверждён (код + новый тест + зелёный Linux CI backend job на точном SHA). Остальные 11 AC не затронуты этим диапазоном и остаются в состоянии r1. Новая Medium-находка (#189) — presentation snap-overlay не режет ось независимой перегородки в месте собственного проёма, вопреки §11 ТЗ и тексту этого же changelog — не проваливает ни один из 12 сформулированных AC буквально, не портит данные, не затрагивает structural/#185-путь и не расширяется на View/kiosk; заведена отдельным issue и не блокирует переход, как и три предыдущих Medium из r1.