mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-01 12:18:51 +00:00
@@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user