Files
houseplan-card/docs/reviews/CODE-REVIEW-359-r1.md
T
2026-08-28 21:49:12 +00:00

18 KiB
Raw Blame History

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 не обновлены.
  • Последствие: это ровно docs job из 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 для неизвестного символа. Smoke previewAndCommitAreIdentical сравнивает реально размещённый 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; smoke unknownSymbolFailsDark форсирует невалидный _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 → в задаче