Issue: #189 User-Visible: no
18 KiB
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, применяется только к sourcekind: '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()— новая опция там просто не используется, а сам метод этим диффом не тронут. Смежный smokesmoke_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 (тот же guardif (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 не заведён).