15 KiB
SPEC-REVIEW-238-r1
- Issue: #238 — «Превью проёма: показывать расстояния до внутренних граней комнаты и стен»
- ТЗ:
docs/specs/238-opening-inner-distances.md - SHA ревью:
0498dda7cc49e5769eee51a402204bba92ac304b(веткаissue/238-opening-inner-distances) - Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Этап: ТЗ на ревью (PROCESS.md §2.4)
- Метка
small/trivialотсутствует (сложность 7/10, новый UX-контракт) → полный трек, спека вdocs/specs/, что и сделано корректно.
Скоуп проверки
Первый заход — разбор полный (§2.10 неприменим до второго цикла). Проверено:
docs/SCOPE.md— привязка к J4/J6, персона «администратор дома», Plan editor desktop-first.AGENTS.md,PROCESS.md§2.4/§7.1/§7.2/§4 — обязательные разделы, лимит циклов, формат вердикта.- Тело issue #238 и оба комментария (аналитика S2, «ТЗ готово к ревью»).
docs/USER-GUIDE.ru.md(терминология проёмов, размеров Resize, инвариант Shift) иdocs/CANVAS.md§9.4,docs/WALL-THICKNESS.md— канонические документы затронутой подсистемы.- Сам файл
docs/specs/238-opening-inner-distances.mdцеликом (406 строк) и запись вdocs/specs/README.md. - Исходный код, на который ссылается диагностика ТЗ (§3, §7–§11):
src/houseplan-card.ts,src/opening-placement.ts,src/logic.ts(openingShoulders),src/wall-thickness.ts(wallIntervals,roomWallProfile,innerContourForRoom,innerEdgeSpan,ownEdgeOffsets),src/physical-geometry.ts(partitionBody,wallBodiesGeometry). - Существование упомянутых регрессионных smoke-файлов и golden-baseline.
Как проверялось
Не код-ревью, поэтому автотесты не гонялись — это ревью ТЗ (§2.4): проверялась выполнимость и проверяемость, а не реализация. Конкретно:
git log/git show 0498dda --stat— коммит содержит толькоdocs/specs/238-opening-inner-distances.mdиdocs/specs/README.md,User-Visible: no,Issue: #238— трейлеры корректны для чисто документационного коммита.grep/Readпоsrc/*.ts— сверка каждого технического утверждения §3 «Подтверждённый диагноз» с реальным кодом (см. таблицу ниже).grep -n shiftKey src/— проверка утверждения AC10 про Shift на предмет «догадки, выданной за факт».- Сверка списка файлов §14 и regression-smoke §17 с реальным деревом (
ls demo/smoke_*.mjs,ls demo/golden/baselines).
Находки
High: 0 · Medium: 0 · Low: 0 (после проверки один кандидат в Low снят как необоснованный — см. ниже).
Блокирующих и требующих правки замечаний нет. Ниже — два пункта, которые были целенаправленно проверены как потенциальные «догадки, выданные за факт», и оба сняты с объяснением, а не молча пропущены.
Проверенный кандидат №1: AC10 и упоминание Shift
AC10: «Centre magnet/tick остаётся осевым; Shift не выключает его...». В коде _resolveOpeningPlacement/_opRuler (src/houseplan-card.ts:11935-12238) нет обращения к shiftKey вообще — центровочный магнит проёма безусловен. На первый взгляд это похоже на факт о поведении, которого в коде нет и который не помечен как предположение (что было бы замечанием по инструкции ревью).
Проверка канона показала обратное: docs/CANVAS.md §9.4 фиксирует общий, не привязанный к конкретному инструменту, инвариант редактора — «Shift never suspends coordinate snapping» — и явно перечисляет, что для прочих инструментов Shift меняет только текущий жест (квадрат/круг, независимые оси resize, компас, обход furniture-магнита), но никогда не снимает базовый snap. AC10 — корректное применение уже существующего канонического правила к новому UX-элементу (центр-магнит проёма), а не изобретённая деталь. Поскольку в текущем коде Shift вообще не читается на этом пути, AC тривиально выполняется и работает как regression guard на будущее — не дефект. Снято, находка не подтвердилась.
Проверенный кандидат №2: §3 не называет innerEdgeSpan/ownEdgeOffsets
§3 «Подтверждённый диагноз» перечисляет уже существующую физическую базу (wallIntervals, roomWallProfile, innerContourForRoom, partitionBody), но не упоминает innerEdgeSpan/ownEdgeOffsets (src/wall-thickness.ts:876-959), которые #233 уже завёл для ровно того же продуктового принципа — «мерить до внутренней физической грани, а не до оси» — и импортированы в houseplan-card.ts:74.
Прочитал обе функции: ownEdgeOffsets сэмплирует толщину соседей по одной точке — середине цельного нарисованного ребра комнаты, а innerEdgeSpan режет это ребро целиком по двум соседним рёбрам. Для Resize (#233) это корректно: перетаскивается всё ребро целиком. Для §238 этого недостаточно по существу: превью проёма стоит в произвольной точке ребра, и на этой точке может лежать не тот атомарный отрезок толщины, что в середине ребра (ребро комнаты может быть склеено из нескольких atomic-интервалов с разной толщиной соседей — ровно то, что описывает модель в docs/WALL-THICKNESS.md §1, «atomic collinear spans»). Требование ТЗ §7.1 п.1 — «участок, покрывающий центр candidate», а не середину всего ребра — поэтому обоснованно требует нового резолвера на уровне atomic-профиля, а не переиспользования #233 «как есть». Отсутствие явной ссылки на innerEdgeSpan/ownEdgeOffsets в §3 — стилистический пробел (было бы полезно явно написать «недостаточно, потому что…», как самому ревью пришлось восстанавливать), но не ошибка и не выдуманное поведение. Не поднимаю как находку — ниже, чем Low, чисто редакторское замечание.
Что проверено и корректно
Построчная сверка §3 «Подтверждённый диагноз» с кодом — всё подтвердилось:
| Утверждение ТЗ | Проверка |
|---|---|
openingShoulders() считает по осевым roomEdges(rooms), не видит толщины |
src/logic.ts:269-308 — использует pts из room.poly напрямую, без offset/half-width |
_resolveOpeningPlacement()/resolveOpeningPlacementResult() — единая точка разрешения candidate для hover/click |
src/houseplan-card.ts:11935,11956; _openingHoverCandidate кеш переиспользуется кликом (:12041-12044, :12588-12598) |
OpMeasure хранит только точку и текст, без геометрии сегмента |
src/houseplan-card.ts:688-691: { labels: {x,y,text}[], guide } — подтверждено, from/to действительно нет |
wallIntervals() даёт half-thickness и roomId по каждому атомарному участку |
src/wall-thickness.ts:1294-1360: WallInterval{roomId, half, open, ...} |
innerContourForRoom()/roomWallProfile() строят внутренний контур через insetContour |
src/wall-thickness.ts:1545-1562 |
partitionBody() строит тело независимой перегородки с её толщиной |
src/physical-geometry.ts:80-85 |
| Дорогой plan-wide boolean union существует и должен избегаться на pointermove | wallBodiesGeometry — тяжёлый вызов на весь space.rooms+walls+openCuts (src/houseplan-card.ts:5175-5190,15583-15588); кеш-инвалидация по _cfgEpoch — реальное поле (:3329, инкрементируется на изменениях конфига, уже используется как ключ кеша структурной геометрии, :6711,6739) |
Разные roomId на одном сегменте → общая стена, распад до legacy при недоказуемости |
Логика в wallIntervals() (по одному входу на каждую комнату) делает это семантически корректным без дополнительной дедупликации в исходнике |
Проверено также:
- Все обязательные разделы §7.1 PROCESS.md присутствуют и в верном порядке (сценарий/что увидит человек — первыми): §1–§21 покрывают сценарий, персону, диагноз/проблему, скоуп и не-скоуп, контракт поведения, модель данных/миграцию, i18n, touch/безопасность, AC1–AC15 с проверяемым доказательством (
unit/smoke/golden/ревью кода), план автотестов, риски, откат, release-артефакты. - AC1 арифметически проверен вручную: ось 400u, проём 80u по центру → legacy плечо (400-80)/2=160u; примыкающая стена глубиной 20u даёт half=10u инсет на каждом конце по правилу «growth half in / half out» (
docs/WALL-THICKNESS.md§2) → 160-10=150u. Число в AC не взято с потолка. - Регрессионные smoke-цели существуют:
demo/smoke_opening_measure.mjs,demo/smoke_opening_preview.mjs— по факту;demo/smoke_partition_opening_jamb.mjsиз текста не существует, но ТЗ прямо и корректно оговаривает это как предположительное имя («сверить с inventory») — реальный файлdemo/smoke_partition_openings.mjsсодержит нужный jamb-margin тест (issue #132/#185/#186). - Golden: существующие
opening-placement-*-thick-wall-{light,dark}.pngвdemo/golden/baselines/подтверждают, что сцена placement-preview для переиспользования/расширения реально существует, как и оговорено в §17 «либо новая минимальная». docs/specs/README.md: строка добавлена в верном месте таблицы, в двустороннем формате issue↔ТЗ, ссылки консистентны.- Не-скоуп (§5.2) корректно опирается на формулировки issue: все три упоминания «размещения»/«превью проёма» в теле issue относятся к новому проёму, а не к drag уже сохранённого — сужение до
AC11/§5.2 не является домысливанием. - §21 «Принятые предположения» — блок присутствует, все 5 пунктов действительно технические/интерпретационные, не продуктовые в смысле §7.1 PROCESS.md (что видит/делает человек уже прямо зафиксировано issue), помечены как оспоримые.
- Аналитика (S2) корректно классифицировала задачу как обычный трек — сложность 7/10 превышает порог
small(≤3). - Откат (§19) реалистичен: persisted-полей нет, откат — чисто код.
Чего не проверял
- Реализацию — её не существует, стадия ТЗ.
- Golden/визуальные baseline для новых 2/4 линий — они появятся только в коде; на этой стадии оценивался только план их получения (§17).
- Полный HA backend/
tests_backend— задача не трогаетcustom_components/**, ТЗ явно это фиксирует (§12), не перепроверялось глубже grep поcustom_components. - Производительность фактическая (только план кеширования по тексту ТЗ, не измерения — на этой стадии измерений и не может быть).
- Не проверял весь
docs/UX-MODES.md/docs/TOUCH-SUPPORT.mdпострочно — только целевые фрагменты про Shift/canvas и точечно про touch/best-effort инвариант, поскольку §13 ТЗ не меняет touch-контракт и это подтверждается отсутствием новых жестов в тексте.
Вердикт
Зелёный. ТЗ выполнимо, каждый AC проверяем и привязан к способу доказательства, диагностика подтверждена чтением исходного кода, продуктовых вопросов владельцу нет — 5 явных предположений корректно оформлены и оспоримы. High: 0, Medium: 0.