17 KiB
SPEC-REVIEW-359-r1
Issue: #359 — Предпросмотр мебели на плане перед размещением
ТЗ: docs/specs/359-furniture-placement-preview.md
Материал: коммит 392ef22c "docs: specify furniture placement preview" (единственный
файл, docs/specs/359-furniture-placement-preview.md, 197 строк; трейлеры
Issue: #359 / User-Visible: no корректны — код не менялся).
Заход: r1 (первый раунд, раздела «Унаследовано»/«Закрытие раунда» нет).
Скоуп
Полный трек (owner-аналитика: small не выполнен — «нарушен критерий "нет
нового UX-контракта"»), поэтому спек живёт файлом в docs/specs/, а не в теле
issue. Диапазон разбора — весь документ ТЗ плюс тело issue #359 и единственный
комментарий владельца (оценка/взятие в работу).
Как проверялось
Ревью документа без исполнения кода:
docs/SCOPE.md— задача закрывает J4/J6 (снижение ошибок первичного размещения мебели, «Keep the plan true» / «zero to plan GUI onboarding»); не задевает лок-инвариант, View/kiosk, ничего из «Out of scope».AGENTS.md,PROCESS.md§2.4/§7.1/§12 — обязательные разделы, формат AC, класс изменения (C — толькоdocs/**, коммит и так закрыт DoD спека).- Тело issue #359 и комментарий-аналитика — сверка скоупа, персоны, поверхности (Background editor, touch best-effort), P2/сложность 4/10.
docs/TOUCH-SUPPORT.md,docs/CANVAS.md§9.4,docs/USER-GUIDE.ru.md(раздел «Мебель», строки 1303–1311) — сверка терминологии и заявленного поведения магнита/Shift/touch с каноном.- Чтение текущего кода без исполнения:
src/furniture.ts(snapFurnitureToWall,furnitureCorners,furnitureResize),src/houseplan-editor-runtime.ts(_furnPlace,_furnMoveUpdate,_decorShapeDown, веткаt === 'furniture'в_decorPointerDown~L4090),src/pointer-modality.ts— проверка технической реализуемости single-resolver и hover-condition claims. demo/golden/matrix.mjs,demo/golden/README.md— сверка заявления «golden не требуется» с существующей практикой (hoverRoom/hoverDevice,junction-draft-end-node-dark).demo/smoke_furniture.mjs,test/furniture.test.mjs— существующее покрытие, на которое опирается план автотестов.
Гейты typecheck/test/build не гонялись: диапазон — один документ в
docs/specs/, продуктовый код не тронут. check-docs.mjs/golden/smoke не
применимы к этапу спек-ревью — они станут предметом код-ревью, когда появится
реализация.
Находки
Medium (в скоупе) — контрактный пункт 9 (invalid/unknown symbol) не имеет доказательства
Файл: docs/specs/359-furniture-placement-preview.md, «Контракт поведения»
п.9 (строка 83) и «Критерии приёмки» (114–141).
Воспроизведение по тексту: п.9 контракта формулирует два независимых
требования: «изменение Width/Depth… пересчитывает preview» и «невалидное/
неизвестное изображение не создаёт preview и не ломает редактор». AC3
доказывает только первую половину («живые размеры»). Ни один из AC1–AC8, ни
«План автотестов» (145–155) не называет проверку для случая, когда у
_furnPalette.symbol нет соответствующего FurnitureGraphic/FurnitureSymbol
(например, furnitureGraphic(id) возвращает null — путь уже существует в
src/furniture.ts:362). Риски (161–174) упоминают это как «Fail dark», но риск
— не доказательство, а декларация намерения; без названного AC/теста это
утверждение контракта останется непроверенным после реализации.
Почему это Medium, а не High: не блокирует реализуемость — поведение однозначно описано («не создаёт preview и не ломает редактор»), только не привязано к способу проверки, как того требует §7.1 DoR («каждый AC… с указанием доказательства»).
Как чинится в скоупе: добавить AC9 (или расширить AC3/AC7) с явным
доказательством — unit-тест резолвера на неизвестный symbol либо smoke-шаг,
проверяющий отсутствие .furniture-placement-preview и отсутствие исключения.
Medium (в скоупе) — «golden не требуется» слабо обосновано на фоне уже существующей практики
Файл: docs/specs/359-furniture-placement-preview.md, «Release-артефакты»
(182–190), обоснование в конце «Плана автотестов» (157–159).
Воспроизведение: обоснование — «smoke проверяет реальный SVG path,
computed style и позиционную геометрию, а принятие нового изображения добавило
бы дорогой платформенный шум к transient editor-only состоянию». Но
demo/golden/matrix.mjs уже содержит именно такие детерминированные transient
и hover-состояния без «платформенного шума»: hoverRoom/hoverDevice
(decor-over-opaque-hover-light:452, hover-over-glow-dark:627,
hover-nested-room-dark:629) и черновик рисования линии
(junction-draft-end-node-dark:668) — оба задаются программно через состояние
карточки, а не реальным движением указателя, то есть детерминированы точно так
же, как предлагает избежать этот спек. Ценность фичи — именно пиксель-точное
совпадение preview с будущим объектом (контракт п.2, UX: «ghost находится в
decor composition layer поверх сохранённого decor»); AC7 доказывает это только
через DOM/computed-style-ассерты (path d, opacity, aria-hidden,
pointer-events), которые не ловят дефекты z-order/композитинга (например,
ghost отрисован позади другого decor-объекта, либо неверный порядок слоёв
относительно сохранённой мебели) — то, что видно только на растровом
сравнении.
Почему это Medium, а не High: не блокирует реализацию и не касается продуктового решения — способ проверки визуального результата чисто технический и не требует вопроса владельцу.
Как чинится в скоупе: либо добавить один детерминированный golden-сценарий
для preview-состояния тем же программным способом, что и
junction-draft-end-node-dark, либо заменить обоснование в «Release-
артефактах» на техническую причину, специфичную для этого случая (а не общее
«transient = шум», которое опровергается двумя уже существующими сценариями в
том же файле).
Low-находок нет.
Что проверено и корректно
- Обязательные разделы §7.1 присутствуют полностью: сценарий, «что человек увидит до/после», проблема, скоуп/не-скоуп, контракт поведения, UX, модель данных и миграция, i18n, AC1–AC8 с доказательством, план автотестов, риски, откат, release-артефакты — плюс обе продуктовые вставки из AGENTS.md (персона/ поверхность/момент в «Сценарии»; факт без терминов реализации в «Что человек увидит»).
- Персона и поверхность корректны и привязаны к J4/J6 из
docs/SCOPE.md; задача не задевает лок-инвариант, View/kiosk, ничего из «Out of scope». - Touch-контракт (п.10, «Не-скоуп») дословно совпадает с
docs/TOUCH-SUPPORT.md: «best effort», допустимое отсутствие hover-preview на coarse pointer, запрет «сохранения непреднамеренной геометрии из-за pinch/ cancel/второго касания» — прямое соответствие разделу «Safety floor that still applies to touch editors». - Магнит к стене и роль
Shift(контракт п.3) технически точны и совпадают с каноном и текущим кодом:docs/CANVAS.md§9.4 («bypassing the furniture wall magnet while the ordinary decor/room/grid magnet remains active») иdocs/USER-GUIDE.ru.md:1305-1311слово в слово подтверждают заявленное поведение; вsrc/houseplan-editor-runtime.ts:4653и:4689(_furnPlace/_furnMoveUpdate) уже сегодняev.shiftKey ? null : snapFurnitureToWall(...)— ровно та развилка, которую описывает спек. - Единый resolver (контракт п.5) технически реализуем без притягивания
сущностей.
_furnPlace(L4642-4675) уже сегодня — короткая последовательность чистых вызовов (_decorSnap→snapFurnitureToWall→ clamp → сборкаDecorShape); выделение общей чистой функции, которую вызовут и превью, иpointerdown, не требует новой архитектуры.snapFurnitureToWallвsrc/furniture.ts:418уже чистая и уже покрыта сигнатурой, которую предполагает AC2. - Условие видимости preview (контракт п.1, «последний fine/hover-capable
mouse pointer») опирается на существующий механизм, а не на новый: в
проекте уже есть
src/pointer-modality.tsи задокументированное вdocs/TOUCH-SUPPORT.mdправило «Hover is instance-local and follows the latest real pointer input… enabled only after a mouse event when the browser also reports fine, hover-capable hardware» — спек его переиспользует, а не изобретает. Симметрично п.10 («touch/pen… очищают возможный mouse-preview») — прямая калька с «Touch and pen input immediately clear transient room and device hover» из того же документа. - Терминология UX-раздела корректна: «decor composition layer» совпадает с
комментарием в
src/houseplan-card.ts:11207(«Decor is one composition layer above every floor treatment»); класс decor-слоя (.decorlayer) уже существует (src/houseplan-card.ts:8140). - Значение opacity
0.55не произвольно — то же число уже используется как устоявшийся визуальный язык проекта для «неокончательного/фонового» состояния (src/styles/chrome.styles.ts:151.tab.dragging,src/styles/plan.styles.ts:572,1112, анимации вsrc/styles/base.styles.ts), то есть не гадание, а согласованный выбор. - i18n корректен: новых текстовых строк нет, новых ключей не требуется — документ прав, что раздел можно закрыть без i18n-плана.
- Модель данных/миграция корректны:
DecorShapeне меняется, preview — чисто runtime state; откат тривиален и не требует обратной миграции. - Открытых продуктовых вопросов не найдено. Отдельно проверены места,
которые могли скрывать угаданное продуктовое решение под видом факта:
выбор непрозрачности (обоснован существующим стилем, не изобретён),
отсутствие дополнительной рамки/анимации (дословно повторяет «Предлагаемое
поведение» из тела issue, не добавляет нового решения), поведение на
гибридных touch+mouse устройствах (уже решено существующим
pointer-modality.ts, не новый вопрос). Раздел «Принято предположительно» содержит только технические допущения (имя state, модуль резолвера, способ коалесцирования, состав source-contract тестов) — ровно то, что §7.1 отдаёт автору, а не владельцу. - Не-скоуп чётко ограничивает риск расползания: multi-stamp, размерные плашки, collision detection, миграция схемы, move/resize/rotate уже сохранённой мебели — исключены явно и обоснованно, никаких «попутных» претензий.
Чего не проверял
- Реализуемость AC1–AC8 в реальном браузере (Playwright) — код ещё не написан, это предмет код-ревью на S7.
npm run typecheck/npm test/npm run build/npm run bundle:sync/npm run bundle:budget/node demo/smoke_furniture.mjs— не применимы, диапазон изменения не касаетсяsrc/**,demo/**,test/**.node scripts/check-docs.mjs— не применим,src/**не менялся, отпечаток скриншотов не мог устареть от этого коммита.- Точная формулировка будущих i18n/changelog записей (
docs/FURNITURE.md,docs/USER-GUIDE.ru.md,docs/CHANGELOG*.md) — они появятся вместе с реализацией и будут предметом код-ревью, а не спек-ревью. - Производительность render-цикла на реальном железе — риск назван и выглядит правдоподобно (один SVG path, без пересчёта модели), но это утверждение, которое подтвердит только код-ревью/смок, не документ.
Вердикт
Жёлтый: 0 High, 2 Medium, оба в скоупе задачи #359 и чинятся правкой ТЗ в этом же issue (без отдельного issue, решение владельца 2026-08-19, #202).