18 KiB
CODE-REVIEW-359-r1
Issue: #359 — Предпросмотр мебели на плане перед размещением
SHA: 8b66d67d (issue/359-furniture-placement-preview)
ТЗ: docs/specs/359-furniture-placement-preview.md, spec review r2 — зелёный
Заход: r1 · блокирующих циклов израсходовано 0 из 4 (r1 — первый заход code review, разбор полный)
Скоуп проверки
Диапазон origin/dev..HEAD — 5 коммитов, из них один продуктовый:
8b66d67d feat: preview furniture placement (Issue: #359, User-Visible: yes).
Остальные четыре — спек и спек-ревью документы (r1/r2), уже принятые до этого
этапа.
Диф продуктового кода: src/furniture.ts, src/houseplan-card.ts,
src/houseplan-editor-runtime.ts, src/styles/plan.styles.ts. Плюс тесты
(test/furniture.test.mjs, test/golden-matrix.test.mjs,
demo/smoke_furniture.mjs, demo/golden/harness.mjs, demo/golden/matrix.mjs),
документация (docs/FURNITURE.md, docs/USER-GUIDE.ru.md, оба CHANGELOG) и
бандл/дистрибутив (класс D, обновлён bundle:sync).
Диф не трогает геометрию комнат/толщину/layout/marker.space/open_spans:
_furnWalls используется только на чтение для магнита мебели, схема decor не
меняется. npm run invariants не требуется — подтверждено чтением диффа
(нет изменений в space-geometry/wall-degrade/rekey путях кроме одного нового
импорта clampCanvasN, который уже существует и не меняет поведение).
Как проверялось
Дешёвые гейты прогнаны лично на этом SHA (зелёного Validate на нём нет):
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
чисто, без вывода |
| unit | npm test |
tests 1511 · pass 1510 · fail 0 · skipped 1 |
| build | npm run build |
created dist in 15.3s |
| bundle sync | npm run bundle:sync |
три копии синхронны (custom_components, demo/srv/assets) |
| bundle budget | npm run bundle:budget |
View 272469 B / 282000 B (headroom 9531 B), editor 137196 B |
| furniture pack | npm run furniture:check |
Furniture pack OK: 44 plan symbols, 33 menu icons |
| docs fingerprint | node scripts/check-docs.mjs |
ERROR screenshot source fingerprint is stale — см. находку ниже |
| docs пересборка (диагностика находки) | node demo/docs/capture.mjs |
пересобрал скриншоты/фингерпринт локально, подтвердил причину; изменения отменены (git checkout -- docs/images/), не коммитил |
| browser smoke | node demo/smoke_furniture.mjs |
OK, все 60+ именованных ассертов true |
| golden (новый сценарий) | node demo/golden/run.mjs --mode=capture --scenario=furniture-placement-preview-light |
missing-baseline — ожидаемо, приёмка эталона требует полного Linux CI (AC10, demo/golden/README.md) |
| golden (регрессия) | npm run golden:verify |
все существующие сценарии passed, кроме нового (missing-baseline, ожидаемо) — новая композиция decor-слоя не сдвинула ни один принятый эталон |
| smoke-select (широкий охват) | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
4 файла src/**, 44 изменённых символа-проекта; 31 «прямое совпадение», включая demo/smoke_furniture.mjs (уже прогнан). Остальные «прямые» — общие декор-символы (_curSpaceCfg, _decorTool, _svgPoint, NORM_W, _editorRuntime, _pointers) на диффах в _stagePointerMove/_stagePointerLeave/_stagePointerUp, которые новый код только оборачивает веткой _decorTool === 'furniture', не меняя остальные ветки — прочитаны построчно (см. ниже), не прогонялись целиком: точечный per-tool guard, не общая decor-логика |
Инвариант «один источник числа»: preview и commit используют один и тот же
чистый resolveFurniturePlacement (src/furniture.ts:418) — подтверждено
и unit-тестом (preview и commit равны для одного input), и smoke
(previewAndCommitAreIdentical), и чтением кода (_furnPlace больше не
пересчитывает привязку отдельно, а берёт результат _resolveFurniturePlacement
целиком). Второй показываемый параметр — ширина/глубина в полях палитры — тот
же pal.w/pal.h, что уходит в резолвер; отдельного пути нет.
Находки
High — 1
docs-гейт на этом SHA красный: скриншот-фингерпринт устарел, а PNG не пересобраны.
- Файл:
docs/images/screenshots.json - Воспроизведение:
node scripts/check-docs.mjs→ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs. Причина — числами: записанный в коммитеsourceFingerprint—c94b78394be8a5a7dc2973652aa9aba4b6896cfd4c7f44c0038fadf911e62eb4(тот же, что и наorigin/dev,git show origin/dev:docs/images/screenshots.jsonиgit show HEAD^:docs/images/screenshots.jsonсовпадают — наdevгейт был зелёным). Диф трогаетsrc/furniture.ts,src/houseplan-card.ts,src/houseplan-editor-runtime.ts,src/styles/plan.styles.ts— фингерпринт считается по всемуsrc/**, значит любая правка делает его устаревшим. Пересчитанный локально фингерпринт —5e7ddb2ac8885e8e6edf2113a1f35551ce3338234da9324d0874e3f861205502;docs/images/*.pngиdocs/images/screenshots.jsonв коммите8b66d67dне обновлены. - Последствие: это ровно
docsjob изvalidate.yml— «реальный блокер» (AGENTS.md). На этом SHA он красный детерминированно, не по вкусу CI-раннера. Слияние вdevбез починки повторит #230/#234:devостанется с краснымdocsдо следующей задачи (#237). - Это не про содержание документации —
docs/FURNITURE.mdиdocs/USER-GUIDE.ru.mdобновлены корректно и по делу (см. «Проверено и корректно»). Дело исключительно в шаге релиз-артефактов «пересобрать скриншоты», который ТЗ не назвало явно (в разделе «Release-артефакты» ТЗ вообще не упомянут скриншотный гейт), ноAGENTS.md/PROCESS.mdтребуют его для любой правкиsrc/**, а хендофф автора его не называет и не прогонял. - Почин:
npm run build && node demo/docs/capture.mjs, затемnode scripts/check-docs.mjsдо зелёного, закоммитить обновлённыеdocs/images/*.pngиdocs/images/screenshots.jsonвместе с остальным диффом (или отдельным коммитом класса C/D с тем же issue-трейлером).
Находка в скоупе задачи (сама задача внесла правку в src/**, из-за которой
гейт стал красным) — чинится в этом же issue, отдельный issue не заводится.
Проверено и корректно
- AC1 (появление) —
mouseHoverShowsRealSymbolв smoke: реальный path,data-symbolсовпадает с выбранным символом. - AC2 (геометрический паритет) —
resolveFurniturePlacement(src/furniture.ts:418) — чистая функция, вызывается один раз и для превью (_resolveFurniturePlacement→_furniturePreviewPlacement), и для коммита (_furnPlace). Unit-тест сравнивает результатdeepEqualдля одинакового входа; отдельный тест покрываетfree/Shiftи canvas-guard (clampCanvasN, лимит 5000) иnullдля неизвестного символа. SmokepreviewAndCommitAreIdenticalсравнивает реально размещённыйsofaс зафиксированным значением превью перед кликом —x/y/w/hс точностью1e-12,angleточно. - AC3 (живые размеры) —
sizeUpdatesPreviewWithoutPointerMove: изменение поля Width без нового движения мыши меняетwпревью иtransform. Работает потому, что рендер геометрии превью читает текущий_furnPaletteна каждый рендер (_furniturePreviewPlacement— геттер, не кэш). - AC4 (чистота) —
previewDoesNotMutateConfigOrHistory:_decorList.length,_cfgEpoch, undo/redo-имена не меняются наpointermove. Прочитано в коде:_furnPointerMoveпишет только_furnPreviewInput(@state, не часть конфига) и ничего не передаёт в_saveConfig/_recordGeometry. - AC5 (очистка) —
pointerLeaveClearsPreview,previewClearsAfterCommitв smoke; остальные точки очистки (Escape, кнопки «закрыть»/«назад»/категория палитры, переключение инструмента,_setMode, смена_space,_stagePointerCancel,_clearGeometryGesture) не покрыты ни отдельным smoke-шагом, ни source-contract тестом (ТЗ допускал последний опционально, «если нельзя надёжно доказать одним smoke» — план его не потребовал письменно, но и не реализовал). Проверено чтением, не исполнением: все девять точек вызова_clearFurniturePreview()(src/houseplan-editor-runtime.ts:1178, 2207, 2238, 4722, 4746, 4867, 4878, 4902, 5222, 5345, 5357, 5361иsrc/houseplan-card.ts:1450, 2691, 2797) расположены до любого early-return, который мог бы их обойти — прослежено построчно для_setMode(в самом начале функции, до проверки «тот же режим»), для Escape-ветки (до сброса_furnPalette), для кнопок палитры и переключателя инструментов в_renderDecorBar/_renderFurnPalette/_renderDecorSecondary. Пропусков не нашёл. - AC6 (ввод) —
shiftBypassesTheWallMagnet(былоshiftKeepsTheMagnet— переименовано согласно новому контрактуShift, смок обновлён консистентно с CANVAS.md §9.4 и ТЗ п.3);touchCancelMoveAndSecondContactDoNotSave— четыре синтетических touch-сценария (move>8px, cancel, второй contact,isPrimary:false) не создают запись. Прочитано:_furnPointerMoveпомечаетpending.cancelledпри движении больше 8px (тот же порог, что уже использует_stagePointerMoveдля pan-lock,src/houseplan-card.ts:6220— не новое магическое число), а_decorPointerDownигнорирует второй контакт, не создавая для него собственныйpending. - AC7 (визуальный контракт) —
previewIsTransientAndInert:aria-hidden,pointer-events: none(вычисленный computed style),opacity: 0.55с допуском1e-6. CSS-правило.decorlayer .furniture-placement-preview(src/styles/plan.styles.ts) не задевает никакой другой класс. - AC8 (совместимость) — весь остальной furniture-flow (drag/resize/rotate,
erase, View-рендер) прогнан тем же расширенным smoke и остаётся зелёным;
npm testбез регрессий (1510 pass, 0 fail);npm run golden:verify— все ранее принятые сценарииpassed, ни один не сдвинут новым кодом decor-слоя. - AC9 (неизвестный символ) — unit-тест
resolveFurniturePlacement({..., symbol: 'future_unknown_symbol'})→null; smokeunknownSymbolFailsDarkфорсирует невалидный_furnPalette, проверяет отсутствие ghost и записи, и что инструмент остаётся вооружён для восстановления валидного выбора. - AC10 (композиция) — golden-сценарий
furniture-placement-preview-lightзарегистрирован вmatrix.mjsиtest/golden-matrix.test.mjs; harness (demo/golden/harness.mjs) программно вооружает предмет, ставит курсор и проверяет DOM-контракт (символ,aria-hidden,pointer-events, opacity, порядок относительно[data-hp="wall"]) до самого растрового захвата — капчур прошёл (missing-baseline, эталон не принят локально — верно по правиламdemo/golden/README.mdи поnpm run golden:verify, который не нашёл ни одной существующей регрессии). - Трейлеры:
Issue: #359,User-Visible: yesна продуктовом коммите; оба CHANGELOG (docs/CHANGELOG.md,docs/CHANGELOG.ru.md) правлены в том же коммите8b66d67d. Формулировки соответствуют реальному поведению (сверено построчно с кодом), терминологияdocs/USER-GUIDE.ru.mdиdocs/FURNITURE.mdне придумана заново, согласована с существующим разделом «Мебель». - Ветка/процесс: один продуктовый коммит на весь диф, класс A корректно
отделён от класса D (бандл) — все три копии бандла синхронны
(
bundle:sync), бюджет в пределах (bundle:budget).
Чего не проверял
python -m pytest tests_backend— не прогонялся: диф не касаетсяcustom_components/**/*.py.- Performance-профили — не прогонялись: не названы в AC, диф не трогает чувствительные к перфу пути (рендер одного дополнительного SVG-path на pointermove — цена признана в ТЗ и не требует профиля).
npm run invariants— не прогонялся: диф не меняет рёбра комнат, записи толщины,layout,marker.space,open_spans;_furnWallsиспользуется только на чтение существующим (не новым) резолвером стен.- Полный
demo/smoke_*.mjsнабор — не прогонялся целиком; выбор сужен инструментомsmoke-select.mjs(см. таблицу выше) доsmoke_furniture.mjs, который прогнан. Остальные «прямые совпадения» разобраны чтением как точечные ветки существующей decor pointer-логики, не общие пути. docs/images/*.pngвизуально — не сверял пиксель-в-пиксель новые скриншоты с прежними (не требуется: находка в том, что фингерпринт вообще не пересобран в коммите, а не в содержимом самих кадров).
Вердикт
Функционально фича сделана верно и полно: единый resolver, паритет
preview/commit, полная очистка по всем перечисленным в контракте границам
(включая девять точек, проверенных чтением), touch/pen fail-safe, unknown-symbol
fail-dark, новый golden-сценарий и расширенный smoke — всё воспроизводимо и
падение тестов проверяемо (пробовал ломать резолвер мысленно на canvas-guard —
тест зафиксировал бы -5000, а не что-то другое; unknown-symbol тест ловит
регрессию furnitureGraphic напрямую).
Единственная находка — не в продуктовой логике, а в release-артефакте:
скриншотный гейт документации остался красным на этом SHA. Это High по
формальному эффекту (гарантированно красный обязательный job docs в
validate.yml при слиянии) и по прецеденту (#230/#234/#237), поэтому
вердикт — красный, а не жёлтый с отложенным High.
Вердикт: красный · заход r1 · блокирующих циклов 0/4 · High: 1 · Medium: 0 → в задаче