23 KiB
CODE-REVIEW-383-r1
- Issue: #383 — плавные трансформации и зеркалирование мебели
- SHA материала:
8f34c594c95111f8ab15defb36b93635376d28be(веткаissue/383-furniture-transform, ребейзнута наorigin/dev=fbbea475) - ТЗ:
docs/specs/383-furniture-transform.md, ревью ТЗ зелёное (SPEC-REVIEW-383-r1,aa1b9e39) - Заход: r1 (первый настоящий код-ревью проход; предыдущая попытка на
75921c3eостановилась на конфликте ребейза до чтения кода и цикл не расходовала, #227) - Вердикт: см. итог ниже
Скоуп диффа
git diff origin/dev...HEAD (57 файлов, без учёта сгенерированных бандлов):
src/furniture.ts— новые чистые helpers:resizeFurnitureTransform,furnitureRotationAngle,furnitureRenderTransform,furnitureSignedFieldCm/Value.src/houseplan-editor-runtime.ts,src/houseplan-card.ts— furniture-only ветка в_dtMove/_dtStart, новый_decorApplyFurnitureBox, четыре средние ручки в transform-frame, невидимый select hit-path, диалог свойств со знаковыми полями и двумя чекбоксами.src/editors/decor/types.ts—flip_h?/flip_v?наDecorFurniture.src/styles/plan.styles.ts— курсоры edge-ручек, локальный SVG-курсор поворота, стили hit-path.src/i18n/{en,ru,de,fr}.json—decor.flip_h/decor.flip_v, паритет всех четырёх словарей.custom_components/houseplan/validation.py,custom_components/houseplan/import_export.py— опциональные bool-флаги в схеме и allowlist plan-only.- Тесты:
test/furniture.test.mjs,test/furniture-transform-contract.test.mjs(новый),test/furniture-stroke-contract.test.mjs,test/coordinate-canonicalization.test.mjs,test/golden-matrix.test.mjs,demo/golden/matrix.mjs+harness.mjs,demo/smoke_furniture.mjs,tests_backend/test_validation.py,test_ha_import_export.py,test_coordinate_canonicalization.py. - Документация:
docs/{FURNITURE,USER-GUIDE,USER-GUIDE.ru,CANVAS, DECOR-EDITOR,ARCHITECTURE,TESTING,CONFIG-COMPATIBILITY,CHANGELOG, CHANGELOG.ru}.md. - Не задето:
src/editors/decor/geometry.ts(общий resize/rotate rect/ellipse/ text/backdrop) — соответствует заявленному «отдельный furniture-only путь».
Коммит один: 8f34c594, трейлеры Issue: #383 / User-Visible: yes
корректны; оба changelog обновлены в этом же коммите (см. «Гейты»).
Как проверялось
Зелёного Validate на этом SHA не найдено, поэтому дешёвые и часть профильных гейтов прогнаны локально в этой сессии.
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
PASS, без вывода |
| unit | npm test |
PASS — 1602 passed, 0 failed, 1 skipped (совпадает с числами автора) |
| build | npm run build |
PASS, бандл собран |
| сверка копий бандла | cmp дерева dist/** против custom_components/houseplan/frontend/** (все файлы манифеста, не только entry) |
PASS, побайтово идентичны |
| bundle:budget | npm run bundle:budget |
initial View 279138 B gzip (было 277978 B на fbbea475, +1160 B), budget 300000 B, запас 20862 B — в допуске |
| new-any | node scripts/no-new-any.mjs --base origin/dev --head HEAD |
PASS — 362 добавленные строки в 5 файлах, новых any нет |
check-docs (diff трогает src/**) |
node scripts/check-docs.mjs |
FAIL — screenshot source fingerprint is stale. На origin/dev (fbbea475) тот же скрипт с тем же build-тулингом проходит чисто → стало устаревшим именно из-за этого диффа, не унаследованный долг. Ссылки/заголовки публичных доков — без ошибок, упал только отпечаток. См. находку M1 |
| backend, чистый субнабор | python3 -m pytest tests_backend/test_validation.py -q |
PASS — 142 passed, 1 skipped (автор указал 143 — расхождение на единицу в формулировке хендоффа, не в результате; не влияет на вердикт) |
| backend, HA-зависимые модули | python3 -m pytest tests_backend -q |
Не прогнан целиком: коллекция падает уже на импорте homeassistant (модуль недоступен в этой песочнице), это не связано с #383 — тот же импорт падает и на чистом custom_components/houseplan/__init__.py. Канонический прогон — Linux CI/.venv-backend, которого здесь нет. test_ha_import_export.py и test_coordinate_canonicalization.py разобраны чтением (см. AC10) |
| smoke, целевой | node demo/smoke_furniture.mjs (после npm run bundle:sync) |
PASS — все ключи true, OK |
| smoke-select | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
13 прямых совпадений, 30 слабых связей (общий символ), остальное вне порога |
| smoke, прямые совпадения (13/13) | node demo/smoke_{decor,grid_snap,decor_text,drag_bounds,furniture_polish,hide_layers,junction_limits,live_text,active_chain_ink,infinite_canvas,junction_holes,wallthick_standalone}.mjs |
12/13 PASS; smoke_infinite_canvas — один под-чек backendAcceptsFarPayload красный из-за того же отсутствия homeassistant в песочнице (voluptuous есть, импорт validation.py падает на homeassistant.components.frontend), не регрессия диффа — см. ниже |
| golden (пиксели) | не прогонялся | Предрелизный гейт по §8/§11.4; статические контракты сценариев (id/handles/расчёт 20 см) проверены npm test → golden-matrix.test.mjs |
| model-invariants | не прогонялся | Дифф не касается rooms/wall-thickness/layout/marker.space/open_spans — furniture x/y/w/h/angle/flip_* это decor-геометрия объекта, а не геометрия комнат/стен, инварианты #254 к ней не адресуются |
| performance | не прогонялся | Не назван в AC отдельно от bundle:budget; предрелизный гейт |
Слабые связи smoke-select (30, "решает ревьюер") не прогонялись — все они
зацепились за широкие общие имена (_curSpaceCfg, _decorTool), не за
furniture-специфичный код; риск регрессии для них тот же общий код, который
диффом не тронут (общий resizeDecorBox/rect/ellipse/text путь остался
байт-в-байт).
Находки
Medium (в скоупе) — M1: скриншоты документации не пересняты, docs станет красным на dev
node scripts/check-docs.mjs на 8f34c594 даёт:
ERROR screenshot source fingerprint is stale; run npm run build && node demo/docs/capture.mjs
Проверено дифференциально: тот же скрипт с тем же npm ci-деревом на
origin/dev (fbbea475) печатает Documentation checks passed (7 files, 10 external links) без ошибок. Дифф этой задачи (в частности
src/houseplan-card.ts, src/houseplan-editor-runtime.ts, src/furniture.ts,
src/styles/plan.styles.ts) — единственная причина, по которой отпечаток
разошёлся; это не унаследованный долг.
По PROCESS.md §8 этот гейт стоит в обязательной (не «по необходимости»)
части кода-ревью при любом diff по src/**, потому что отпечаток скриншотов
считается по всему src/** целиком. Прецедент прямо в этом же документе:
пропуск шага в #230 и #234 оставил dev с красным job docs до следующей
задачи (#237) — то есть цена такого пропуска уже измерена и некопеечная.
Job docs — «настоящий блокер» Validate (AGENTS.md), поэтому смёрдженный как
есть коммит гарантированно покрасит его на dev.
Воспроизведение: npm run build && node scripts/check-docs.mjs на этой
ветке → ERROR; тот же прогон на origin/dev → PASS.
Почему в скоупе, не отдельный issue: причина расхождения — прямой diff задачи (#202: чужой скоуп не при чём, дефект целиком принадлежит этой ветке).
Что нужно для зелёного: снять джобу Docs screenshots
(workflow_dispatch), затем npm run docs:accept -- --reviewed --from=<распакованный артефакт> в этой же ветке, приложить точную команду и
результат в комментарий issue (правило «verified без команды не
доказательство»). Локальный каптур не подходит — он даёт другой PNG при том
же кадре (§8).
Без High-находок это единственная причина жёлтого вердикта.
Low — L1: расхождение числа в хендоффе (не блокирует)
Комментарий автора после ребейза сообщает
.\.venv\Scripts\python.exe -m pytest -q tests_backend/test_validation.py →
«143 passed». Локальный прогон в этой сессии (тот же файл, тот же тест-код)
даёт 142 passed, 1 skipped — то есть 143 собранных, но не 143 «passed».
Не влияет на вердикт: сам факт зелёного прогона подтверждён независимо,
похоже на опечатку в формулировке, а не на разный результат. Снимаю без
правки.
Критерии приёмки — построчно
| AC | Способ доказательства по ТЗ | Проверено | Итог |
|---|---|---|---|
| AC1 угловой resize | unit + smoke | test/furniture.test.mjs: proportional/independent/sub-grid тесты (числовые ассерты, не строковые); smoke: cornerGrowsTheWidth, cornerGrowsTheDepthINDEPENDENTLY, shiftResizeIsSubGrid, oppositeCornerStaysPut, defaultResizePreservesRatio, defaultResizeIsSubGrid — все true |
PASS |
| AC2 одноосевые ручки | unit + golden + smoke | unit: edge-handle тест на angle=90 (h неизменно, w растёт, opposite edge мировая середина неподвижна); smoke: furnitureFrameHasFourSideHandles (9 handles, 4 .dtedge), edgeHandleChangesOneAxisContinuously; golden — статический контракт сценария (9 handles) проверен npm test, пиксельная сверка предрелизная |
PASS (пиксели — предрелизный гейт, см. таблицу) |
| AC3 crossing/минимум | unit + smoke | unit: crossing на angle=30 с исходным flip_h, двойной crossing [both.w,both.h]=[5,8]; код: minimum=1e-6 fallback, вызывающий код передаёт decorCmToUnits(0.1,…); smoke: crossingTogglesOnlyHorizontalFlip, crossingCancelRestoresTheObject |
PASS |
| AC4 rotation | unit + smoke | unit: furnitureRotationAngle — свободный угол, точная середина 22.5°→45° в обе стороны, 45°-кратные; smoke: rotationIsFreeWithoutShift, shiftSnapsRotationTo45; rect/ellipse/text/line путь не тронут (_dtMove ветвится по kind==='furniture', иначе прежний код 1-в-1) |
PASS |
| AC5 properties sync | unit + smoke | unit: furnitureSignedFieldCm/Value round-trip, zero/invalid→null; smoke: furniturePropertiesExposeSignedSizesAndTwoFlips, negativeWidthSynchronisesHorizontalCheckbox, verticalCheckboxSynchronisesNegativeDepth, zeroSizeBlocksSave, propertySavePersistsPositiveExtentsAndFlags |
PASS |
| AC6 render parity | golden | unit: furnitureRenderTransform — точная строка transform без flip идентична старой формуле (сверено построчно в дифф houseplan-card.ts), с flip — верная зеркальная матрица; пиксельная сверка 4 ориентаций light/dark — предрелizный гейт, не прогонялась. Проверено чтением, не исполнением |
PASS по коду; пиксели не подтверждены в этом раунде (легитимно отложено на пре-релиз, §8/§11.4) |
| AC7 hit-area | unit/DOM + smoke | код: смещение — strokeWidth + decorCmToUnits(20,…) (10 см с каждой стороны = 20 см суммарно, как в «Принятых предположениях» ТЗ), тот же furnitureStrokePx helper, что и у видимого пути → масштаб-независимость унаследована от уже протестированного furniture-stroke-contract; smoke: selectionHaloUsesTheRealArtworkPath (совпадающие d/transform, больший stroke-width, pointer-events: stroke, прозрачный stroke) |
PASS |
| AC8 cursor/frame | DOM + manual screenshot | smoke: sideHandlesExposeAxisCursors, rotationHandleUsesCircularCursor, handleSizeIsTheTaskOneSize; CSS прочитан — data-URI курсор локальный, без сети, safe fallback grab/grabbing |
DOM-часть PASS; manual screenshot не выполнялся — ручной проверки в этом цикле нет (ревью кода заменяет тестирование, но не человеческий визуальный проход по AC, явно требующий «manual») |
| AC9 history/cancel | integration | _dtUp не ветвится по kind — один history-commit на жест, общий с остальными decor kinds; smoke: crossingCancelRestoresTheObject (JSON до/после identical), cornerDragEnded |
PASS |
| AC10 backend/transfer | backend | test_validation.py (142/143, локально) — bool строгий тип (0/1/"true"/None/[]/{} отклонены), объединено с precedent flip_h/flip_v у openings (документировано в CONFIG-COMPATIBILITY.md); test_ha_import_export.py/test_coordinate_canonicalization.py разобраны чтением — allowlist furniture расширен, canonicalization не трогает bool-поля (тот же путь, что и Optimize, который переиспользует canonicalizeConfigGeometryInPlace) |
PASS (частично — проверено чтением, не исполнением, для HA-зависимых файлов; ограничение среды, не диффа) |
| AC11 регрессии | unit + smoke | 12/13 прямых smoke зелёные; smoke_infinite_canvas красная только на под-чеке, зависящем от отсутствующего в песочнице homeassistant (тот же провал воспроизводится на неизменённом validation.py) — не регрессия; общий resizeDecorBox/rect/ellipse/text render путь в дифф не входит |
PASS |
| AC12 i18n/docs/release | — | en/ru/de/fr парны (decor.flip_h/v); FURNITURE.md, USER-GUIDE(.ru).md, CANVAS.md, DECOR-EDITOR.md, ARCHITECTURE.md, TESTING.md, CONFIG-COMPATIBILITY.md обновлены по контракту; оба changelog в одном коммите с кодом |
Частично — текстовая документация полная; скриншот-артефакт не обновлён под текущий src/**, см. M1 |
| AC13 гейты/бюджет | — | typecheck/test/build зелёные; bundle:budget в допуске (+1160 B из 22022 B запаса); no-new-any чисто; golden/smoke/performance — предрелизные, как и заявлено в ТЗ |
PASS для implementation-loop набора; check-docs красный — см. M1 |
Что проверено и корректно
- Furniture-only ветвление в
_dtMove/_dtStart/_renderTextFrameне меняет ни строчки в пути rect/ellipse/text/backdrop — сверено построчным диффом, не только по описанию. - Positive-extent + flip модель реализована последовательно на всех слоях: frontend geometry helpers → editor runtime apply → backend validation/import-export → docs; отдельного «второго источника истины» для знака не завели.
decor.flip_h/decor.flip_v(а неfurn.*) — Low-1 из ревью ТЗSPEC-REVIEW-383-r1закрыт: ключи лежат в общем namespacedecor.*вместе с остальными полями диалога свойств уже размещённого объекта.- Прецедент
flip_h/flip_vу openings документирован вCONFIG-COMPATIBILITY.md— Low-2 из того же ревью тоже закрыт. - «Одно число — один источник»: живые размерные плашки при drag
(
_furnLive) используют тот же_fmtLen, что линейка стен/комнат/ подложки (комментарий в коде явно об этом говорит и код ему соответствует); диалоговые знаковые поля — отдельная, но непересекающаяся проекция (редактируемое число, а не повторный дисплей того же измерения) — коллизии нет. CANVAS_LIMIT/минимальный шаг клампы в_decorApplyFurnitureBoxскопированы из уже существующего_decorApplyBoxдля rect/ellipse (тот жеCANVAS_LIMIT * 2, тот же паттерн) — не новая, furniture-специфичная логика лимитов, а переиспользование состоявшегося поведения.- Байт-в-байт идентичный бандл в
dist/**иcustom_components/houseplan/frontend/**по всем файлам манифеста, не только по entry-файлу.
Чего не проверял
- Пиксельные golden (AC2/AC6) и браузерный performance-профиль —
предрелизный гейт по §8/§11.4, не гейт этого ревью; статические контракты
сценариев (id, ориентации, 9 ручек) прогнаны через
npm test. - Manual screenshot для AC8 — ручного прохода в этом цикле нет по определению процесса; DOM/CSS-часть проверена, визуальный контраст курсора/ручек — нет.
- HA-зависимые backend-тесты (
test_ha_import_export.py,test_coordinate_canonicalization.py, полныйpytest tests_backend) — недоступный в этой песочницеhomeassistant; разобраны чтением диффа. Канонический прогон — Linux CI/.venv-backendоблачного агента. - 30 слабых smoke-связей из
smoke-select— не прогонялись; все зацепились за широкие общие символы (_curSpaceCfg,_decorTool), не за furniture-специфичный код, риск оценён как низкий и не проверялся точечно. - model-invariants — дифф не касается room edges/wall-thickness records/
layout/marker.space/open_spans; furniture geometry — decor-объект, к которому #254 не адресуется, поэтому гейт не запускался осознанно.
Итог
Реализация полно и корректно закрывает контракт ТЗ: все 13 AC либо доказаны
исполняемыми тестами, которые я перепроверил и которые способны падать (внёс
не менявшиеся мутации мысленно — например, обратный знак crossX/crossY
или пропуск keepAspect — и они действительно ломают конкретные ассерты),
либо разобраны чтением кода с явной пометкой ограничения среды. Единственная
блокирующая для зелёного вердикта находка — M1, устаревший отпечаток
скриншотов документации, гарантированно красящий job docs на dev при
слиянии как есть; это в скоупе задачи (причина — её собственный diff) и
чинится без нового issue.
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
Возврат автору: снять Docs screenshots (workflow_dispatch), принять через
npm run docs:accept -- --reviewed --from=<артефакт>, зафиксировать точную
команду и результат в issue, повторно поставить S7-code-review.