mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 05:08:53 +00:00
207 lines
18 KiB
Markdown
207 lines
18 KiB
Markdown
# CODE-REVIEW-359-r1
|
||
|
||
Issue: [#359](https://github.com/Matysh/houseplan-card/issues/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 → в задаче
|