Files
houseplan-card/docs/reviews/CODE-REVIEW-189-r1.md
T
claude[bot] 5d04e9b7c2
Validate / docs (push) Failing after 38s
Validate / provenance (push) Successful in 54s
Validate / changes (push) Successful in 45s
Validate / process-gate (push) Successful in 54s
Validate / hacs (push) Skipped
Validate / hassfest (push) Skipped
Validate / frontend (push) Skipped
Validate / smoke (push) Skipped
Validate / golden (push) Skipped
Validate / performance_smoke (push) Skipped
Validate / backend (push) Skipped
docs: review document for #189
Issue: #189
User-Visible: no
2026-08-19 01:35:50 +00:00

18 KiB
Raw Blame History

CODE-REVIEW-189-r1

Issue: #189 · трек: trivial (§5.1) · родитель: #132, найдено при код-ревью #132 r2 (docs/reviews/CODE-REVIEW-132-r2.md). Ветка: issue/189-partition-snap-gap, единственный коммит 89789d8 поверх origin/dev (6846ffb).

Скоуп

Диапазон git log --oneline origin/dev..HEAD = один коммит 89789d8 fix: cut partition snap axes at hosted openings, трейлеры Issue: #189 · User-Visible: yes.

Изменённые файлы (git diff origin/dev...HEAD --stat):

  • src/houseplan-card.ts — +1 строка, класс A: передаёт уже существующий _partitionOpeningCuts(space) в buildPlanSnapGeometry();
  • src/plan-snap-overlay.ts — класс A: новая опция partitionCuts, сгруппированная по hostId, применяется только к source kind: 'partition' с совпадающим id;
  • test/plan-snap-overlay.test.mjs — 2 новых unit-теста (класс B);
  • demo/smoke_plan_snap_overlay.mjs — новый партиционный host-опенинг в фикстуре, 3 новые проверки (класс B);
  • docs/CHANGELOG.md, docs/CHANGELOG.ru.md — бюллетень RU+EN, тот же коммит, User-Visible: yes выполнено, терминология («независимый отрезок «Стены»») совпадает с docs/USER-GUIDE.ru.md:430,444,449;
  • dist/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js, demo/srv/assets/houseplan-card.js — три копии бандла, класс D, в том же коммите, что источник.

Затронутая подсистема — presentation snap-overlay Плана (docs/CANVAS.md, раздел «Architectural connection overlay»); контракт там сформулирован универсально («Door, window, gate and intentionally open-span intervals are cut from presentation axes») — код был рассинхронизирован с этим текстом только для independent-partition host, поэтому CANVAS.md не требует правки в этом коммите (сам текст канона не был неверным). Job — J4 (точное редактирование плана инструментом «Стены»); правка убирает ложный snap-кандидат внутри физически несуществующей кладки.

Trivial-трек оправдан: одна поверхность (snap-overlay/resolver Плана), три AC, ожидаемое поведение уже зафиксировано в docs/specs/132-partition-openings.md §11 и в тексте docs/CANVAS.md; решать было нечего, только чинить рассинхрон.

Как проверялось

Гейт Команда Результат
Typecheck npx tsc --noEmit green
Unit npm test green, 887/887 (совпадает с заявленным в хендоффе)
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 green, три копии идентичны, SHA-256 257c984e… совпадает с хендоффом
Целевой smoke (назван в AC1) node demo/smoke_plan_snap_overlay.mjs green, все 35 проверок true, включая новые partitionOpeningGapHasNoLine, partitionOpeningGapDoesNotActivateSnap, oneLinePerSolidInterval (12→13)
Смежный smoke (структурный граф, AC3) node demo/smoke_partition_openings.mjs green, 12/12, включая openingKeepsRoomFaceAxisContinuous: true
Мутационная проверка «тест умеет падать» git checkout HEAD^ -- src/houseplan-card.ts src/plan-snap-overlay.ts, пересборка, cp dist/... demo/srv/assets/..., повтор npm test и smoke упал — unit: 886/886 pass, 1 fail (a hosted opening cuts only its presentation partition axis…); smoke: 3 red (oneLinePerSolidInterval, partitionOpeningGapHasNoLine, partitionOpeningGapDoesNotActivateSnap), остальные 32 не задеты
Восстановление git checkout HEAD -- src/houseplan-card.ts src/plan-snap-overlay.ts, пересборка, cp во все три копии дерево чистое (git status пусто), хэш бандла тот же 257c984e…
git diff --check — green, без пробельных ошибок

Не прогонялись, сознательно:

  • Полный набор demo/smoke_*.mjs (127 шт.) — diff задевает только presentation snap-overlay/resolver Плана. Прогнаны названный в AC1/AC3 smoke плюс ближайший смежный, покрывающий тот же host-механизм (smoke_partition_openings). Полный набор — предрелизный гейт (§8), не гейт код-ревью.
  • npm run golden:verify — diff может в принципе менять видимый результат (это рендер-слой), но проверено чтением фикстур: golden-сценарии plan-snap-endpoint-light/plan-snap-line-gaps-dark и вообще все сценарии mode: 'plan' используют пространства golden-geometry/golden-lighting (demo/fixtures/visual-matrix.mjs:64-127), и ни один из openings[] там не несёт поле host. Значит _partitionOpeningCuts(space) для этих фикстур возвращает [] независимо от диффа, и partitionCuts.get(partition.id) || [] даёт тот же пустой массив, что и раньше (cuts: []) — новый код-путь для golden-баз инертен, пересборка не может изменить ни один пиксель. Если фикстуры изменятся в будущем и приобретут hosted-проём — это будет уже другой диф, не этот.
  • python -m pytest tests_backend -q — Python не тронут (diff не касается custom_components/houseplan/**/*.py).
  • Performance-профили — не названы в AC; добавленный код — построение Map по уже посчитанному _partitionOpeningCuts() (эта функция уже вызывалась для physical/light геометрии в нескольких других местах, houseplan-card.ts:4809,12640,14678) плюс уже существующий cutSegments() на один источник больше — не расширяет асимптотику снапшота.

Проверка AC

  • AC1 — gap на независимой перегородке. Доказано unit (test/plan-snap-overlay.test.mjs, 'a hosted opening cuts only its presentation partition axis without boundary nodes': делит ось на два интервала, границы cut не входят в geometry.endpoints, resolvePlanSnap внутри gap — null) и smoke (partitionOpeningGapHasNoLine, partitionOpeningGapDoesNotActivateSnap, cutBoundariesAreNotEndpoints, uniqueSourceEndpointsOnly не выросло с 11 при добавлении cut — граница не стала узлом). Мутационно подтверждено, что оба доказательства чувствительны к регрессии.
  • AC2 — cut изолирован по host. Доказано unit: в том же тесте соседняя параллельная (nearby) и пересекающая (crossing) независимая перегородка с другим id остаются полностью целыми (a[0]===0 && b[0]===100 / a[1]===-50 && b[1]===50) при cut на host. Изоляция обеспечена структурно — partitionCuts это Map<hostId, cuts> (src/plan-snap-overlay.ts:129-136,170), не геометрический поиск пересечений, поэтому "совпадающая по координатам, но другая по id" перегородка тоже не может получить чужой cut — разобрано чтением, случай вырожденный (совпадающие в пространстве независимые перегородки) и отдельно не тестируется, см. Low-1. Composite room-wall (тот же контракт, что зафиксирован в #132): код-путь _roomWallOpeningInputs()/_planSnapOpeningCuts(), который режет room-ось для composite-хостов, этим диффом не тронут (diff в houseplan-card.ts — ровно одна добавленная строка, не изменяющая существующие функции) — «сохраняет уже действующий physical gap на room-оси» доказано тем, что затрагивающий её код не менялся.
  • AC3 — structural topology не меняется. Доказано unit ('partition axes remain continuous when hosted cuts are omitted from a structural snapshot': без partitionCuts ось остаётся одним сегментом, resolvePlanSnap внутри бывшего gap возвращает kind: 'line') и разбором: _planStructuralGeometrySnapshot() (houseplan-card.ts:6287-6307) не передаёт partitionCuts в buildPlanSnapGeometry() — новая опция там просто не используется, а сам метод этим диффом не тронут. Смежный smoke smoke_partition_openings.mjs (openingKeepsRoomFaceAxisContinuous: true) и полный unit-прогон (887/887, включая существующие тесты #185) не показали регрессии.

Находки

Low-1 — нет теста на composite-hosted проём после этого диффа

Файл: src/houseplan-card.ts:6274 в связке с partitionOpeningHasCompositeRoomWall (src/partition-openings.ts:134-168).

Сценарий: _partitionOpeningCuts(space) вызывается с accept: () => true по умолчанию — он не отличает composite-hosted проём (партиция точно совпадает с производной стеной комнаты) от независимого. Значит для composite-хоста теперь ДОПОЛНИТЕЛЬНО (сверх уже существовавшего room-cut) режется и partition-kind источник той же партиции. Разбором это безопасно: обе точки разреза вычисляются из одного и того же resolvePartitionOpening(...) (opening.rx/ry/angle/rlen синхронизированы с resolved.center/length/angle при partitionHost, :7932-7938), то есть совпадают в мировых координатах в пределах эпсилон; а дедупликация в buildPlanSnapGeometry ранжирует источники room(0) < partition(2) и коллапсирует совпадающие по точным координатам подсегменты в один. Ни один существующий unit/smoke сценарий не комбинирует composite-hosted проём с этой веткой кода, поэтому фактическое поведение (одна линия vs. две перекрывающиеся с разными внешними границами, если производная стена комнаты и партиция не совпадают в габаритах ровно от угла до угла) не подтверждено исполнением, только чтением.

Почему не Medium: код-путь, ответственный за AC2/AC3 (room-cut и структурный снапшот), этим диффом не менялся — риск регрессии там отсутствует структурно. Дополнительное поведение для composite — не требование ни одного AC, а безопасный побочный эффект переиспользования уже существующей функции; максимум, что он может дать в худшем случае — лишнюю, но геометрически верную (по той же физической точке) presentation-линию, не влияющую на snap-резолвер (оба cut в одной точке дают одинаковый gap). Правлю с записью, issue не завожу.

Других находок (High/Medium) нет.

Что проверено и корректно

  • buildPlanSnapGeometry(): новая опция partitionCuts валидирует hostId (строка, не пустая) и конечность точек a/b (finitePoint) — деградирует безопасно (cut отбрасывается, ось остаётся целой) на некорректном входе, а не бросает исключение.
  • Единственный источник partitionCuts для presentation-снапшота — _partitionOpeningCuts(space), уже используемая канонической функцией для physical/light геометрии в трёх других местах (:4809, 12640, 14678); новый вызов не дублирует резолюцию — переиспользует тот же resolvePartitionOpening путь, значит orphan/невалидный host не создаёт cut (тот же guard if (resolution.resolved) cuts.push(...), :7954).
  • _planStructuralGeometrySnapshot() (структурный, #185) и _planSnapOpeningCuts()/_roomWallOpeningInputs() (room-cut, #132) не тронуты этим диффом — единственная новая строка в houseplan-card.ts добавляет параметр в вызов, не меняющий существующие функции. Соответствует комментарию plan-snap-overlay.ts:120-124, тоже обновлённому этим коммитом в соответствии с новым поведением.
  • Smoke-фикстура (base-partition, a:[0.6,0.6] b:[0.9,0.6]) — вне полигонов обеих комнат (left/right заканчиваются на y=0.5), то есть действительно независимая перегородка, а не производная стена комнаты — сценарий корректно воспроизводит основной (не edge-case) кейс из тела issue. Хостовый проём t: 0.8 даёт x = 0.6 + 0.8·0.3 = 0.84, что совпадает с координатой 840 в проверках crosses(...)/eventAt(840, 600). uniqueSourceEndpointsOnly не изменился (11→11) при добавлении cut — прямое browser-подтверждение «границы cut не создают узел», не только в unit.
  • Терминология changelog («независимый отрезок «Стены»») согласована с docs/USER-GUIDE.ru.md:430,444,449, не изобретена.
  • Трейлеры коммита корректны: Issue: #189, User-Visible: yes, оба changelog правлены в том же коммите. Класс D (три копии бандла) идентичны байт-в-байт локальной пересборке — подтверждено cmp/sha256sum дважды (до и после мутационной проверки, на разных состояниях исходника).
  • Нет изменений в i18n, конфиг-миграции, touch-контракте — согласуется с заявлением issue («Нового UX, конфига и i18n нет») и с критериями trivial (§5.1): одна поверхность, ожидаемое поведение уже зафиксировано, решать нечего.

Чего не проверял

  • Полный browser smoke-набор (127 файлов) — не запускал, обоснование выше (объём гейта соразмерен диффу, §8).
  • npm run golden:verify — не запускал; обоснование чтением фикстур выше (ни один golden-сценарий Плана не содержит hosted-проём, код-путь инертен).
  • python -m pytest tests_backend — не запускал, Python не тронут.
  • Performance-профили — не запускал, не названы в AC и не расширяют асимптотику существующего снапшота.
  • Composite-hosted проём как отдельный интеграционный/smoke-сценарий — не запускал, разобран только чтением (Low-1).
  • Ручной запуск карты в браузере вне smoke-харнеса — не делал; фазы ручного тестирования в процессе нет, AC доказаны автотестами плюс разбором кода.

Вердикт

Зелёный. Все три AC доказаны — AC1 unit+smoke, AC2 unit плюс чтение неизменённого room-cut пути, AC3 unit плюс чтение неизменённого структурного снапшота. Оба новых теста подтверждённо умеют падать (мутационная проверка). Trivial-трек оправдан. High: 0, Medium: 0, Low: 1 (правлена записью, issue не заведён).