26 KiB
CODE-REVIEW-383-r2
- Issue: #383 — плавные трансформации и зеркалирование мебели
- SHA материала:
b1b1e7439b5e0e20bfa47c316065c814ec534070(веткаissue/383-furniture-transform, ребейзнута наorigin/dev=95115de2b...(docs: review document for #385)) - ТЗ:
docs/specs/383-furniture-transform.md, ревью ТЗ зелёное (SPEC-REVIEW-383-r1,aa1b9e39) - Заход: r2 · блокирующих циклов израсходовано 1/4
- Вердикт: зелёный (см. итог)
Почему разбор в этом заходе ПОЛНЫЙ, а не только по дельте M1
Формально между r1 и r2 в задаче случилось два события:
- Автор закрыл M1 (устаревший отпечаток скриншотов) отдельным коммитом
b1b1e743— это чистая, локальная дельта (толькоdocs/images/**). - Но перед этим ветка была повторно ребейзнута на ушедший вперёд
dev: r1 проверял8f34c594(базаfbbea475), а текущая ветка стоит на95115de2— между базами леглиfbbea475→…→95115de2, в том числе два реальных fix-коммита по #385 (223f499e,e561e5fc), трогающиеsrc/houseplan-editor-runtime.tsиcustom_components/houseplan/import_export.py— те же файлы, что и #383.
Инструкция прямо называет это одним из случаев, где сокращать анализ до
дельты нельзя: «ребейз на ушедший вперёд dev — после ребейза это другой
код». Поэтому я не ограничился строкой «M1 закрыт» и таблицей соответствий,
а заново прочитал весь диф #383 к origin/dev и заново прогнал гейты на
текущем SHA. Ниже — это полный разбор; таблицы «Закрытие r1» и
«Унаследовано из r1» идут отдельными разделами в конце, как требует формат.
Скоуп диффа (git diff origin/dev...HEAD, без сгенерированных бандлов)
Список файлов не изменился относительно r1 (я сверил построчно): один
логический коммит с продуктовым кодом (686557c5, после ребейза), плюс
докс/ревью-коммиты и коммит b1b1e743 (только docs/images/** +
docs/images/screenshots.json).
src/furniture.ts—resizeFurnitureTransform,furnitureRotationAngle,furnitureRenderTransform,furnitureSignedFieldCm/Value(прочитано целиком заново в этом раунде).src/houseplan-editor-runtime.ts,src/houseplan-card.ts— furniture-ветка в_dtMove, новый_decorApplyFurnitureBox, четыре средние ручки, невидимый select hit-path, диалог свойств.src/editors/decor/types.ts—flip_h?/flip_v?.src/styles/plan.styles.ts— курсоры, hit-path.src/i18n/{en,ru,de,fr}.json.custom_components/houseplan/validation.py,import_export.py.- Тесты:
test/furniture.test.mjs,furniture-transform-contract.test.mjs,furniture-stroke-contract.test.mjs,coordinate-canonicalization.test.mjs,golden-matrix.test.mjs,demo/golden/{matrix,harness}.mjs,demo/smoke_furniture.mjs,tests_backend/{test_validation, test_ha_import_export,test_coordinate_canonicalization}.py. - Документация:
docs/{FURNITURE,USER-GUIDE,USER-GUIDE.ru,CANVAS, DECOR-EDITOR,ARCHITECTURE,TESTING,CONFIG-COMPATIBILITY,CHANGELOG, CHANGELOG.ru}.md, плюсdocs/images/**(M1). - Не задето:
src/editors/decor/geometry.ts— подтверждено повторно.
Проверка на взаимодействие с рёбейзнутыми коммитами. Прочитал оба
привнесённых dev-коммита (223f499e, e561e5fc, оба про маркер-диалог
#385) построчно: правки лежат внутри _renderMarkerDialog (радио/кандидаты
привязки, value_badge/value_source), furniture-код — в _dtMove,
_decorApplyFurnitureBox, диалоге свойств decor и _DECOR_KIND_FIELDS/
DECOR_SCHEMA. Пересечения по функциям и по строкам нет; ребейз прошёл
без конфликтов в исходниках (конфликтовали только сгенерированный бандл и
changelog — подтверждено автором и это видно по git log: коммит
0d5d2896 — публикация ревью r1 — стоит поверх 686557c5, то есть
исходники не переписывались вручную). Риск «новая подсистема» снят.
Как проверялось
Важная оговорка о CI-сигнале. В контексте этого раунда было заявлено,
что Validate на b1b1e743 зелёный и поэтому tsc/test/build можно не
перегонять. Я проверил это через gh run view и обнаружил, что заявление
формально верно (conclusion: success), но вводит в заблуждение по
существу: job'ы Фронтенд, Golden, Смоки, Backend, Hassfest, HACS
в этом раннe — skipped, а не success. Причина механическая: changes
job классифицирует файлы диапазоном BEFORE_SHA..HEAD_SHA, где BEFORE_SHA
— это 0d5d2896 (предыдущий тип этой же ветки, коммит с текстом ревью r1),
а не origin/dev. Пуш b1b1e743 добавил только docs/images/**, поэтому
changes.outputs.frontend=false, и frontend/golden/smoke/backend
получили каскадный skip как зависимые от frontend/changes job'ы (в GitHub
Actions needs-зависимость от skipped-job делает зависимую job тоже
skipped). Реально прогнали и успешно завершили свежий фронтенд-набор
только для 8f34c594 (прошлый SHA, run 33301868911, Фронтенд: success,
но conclusion: failure из-за docs+golden), а его исходники, по пункту
выше, идентичны текущим 686557c5. Единственный реально свежий гейт на
b1b1e743 — preflight (docs+provenance+process-gate) — тот самый шаг,
который закрывает M1, и он зелёный.
Итог: раз «зелёного Validate именно по нужным job'ам на этом SHA» нет, я прогнал дешёвые и целевые гейты сам — это ровно предписанный процессом fallback, не сверх него.
| Гейт | Команда | Результат |
|---|---|---|
| typecheck | npx tsc --noEmit |
PASS, без вывода (5.9s) |
| unit | npm test |
PASS — 1604 passed, 0 failed, 1 skipped (совпадает с хендоффом автора) |
| build | npm run build |
PASS; git status --porcelain пуст после сборки — комитнутый dist/** уже побайтово актуален |
| bundle:sync (3 копии) | npm run bundle:sync |
PASS; git status --porcelain пуст после — dist/, custom_components/houseplan/frontend/, demo/srv/assets синхронны |
| cmp entry | cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js |
идентичны |
| bundle:budget | npm run bundle:budget |
initial View 279147 B gzip, budget 300000 B, запас 20853 B — в допуске (то же число, что видел r1: +1160 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 |
PASS — Documentation checks passed (7 files, 10 external links). Плюс независимое подтверждение через CI preflight на b1b1e743 — реально исполнялся, не переиспользован |
| backend, чистый субнабор | python3 -m pytest tests_backend/test_validation.py -q (после pip install pytest voluptuous, т.к. песочница голая) |
PASS — 142 passed, 1 skipped |
| backend, HA-зависимые модули | python3 -c "import homeassistant" → ModuleNotFoundError |
Недоступны в этой песочнице; воспроизвёл, что тот же импорт падает и на неизменённом custom_components/houseplan/__init__.py (from homeassistant.components.frontend import add_extra_js_url) — ограничение среды, не диффа. test_ha_import_export.py/test_coordinate_canonicalization.py разобраны чтением (см. AC10 ниже) |
| smoke, целевой | node demo/smoke_furniture.mjs (после bundle:sync) |
PASS — все ключи true, OK |
| smoke-select | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
13 прямых совпадений, 30 слабых связей — идентично r1 (исходники не менялись) |
smoke, прямые совпадения (12 из 13 сверх furniture) |
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 |
11/12 PASS; smoke_infinite_canvas — один под-чек backendAcceptsFarPayload красный по той же причине отсутствия homeassistant (проверено: тот же провал воспроизводится на неизменённом validation.py) |
| golden (пиксели) | не прогонялся | Предрелизный гейт по §8/§11.4, как и в r1; статические контракты сценариев (id, 9 handles, 4 dtedge) проверены npm test → golden-matrix.test.mjs (PASS, 44/44) и новым ассертом в demo/golden/harness.mjs (decorSelection → проверка 9/4 хендлов) |
| model-invariants | не прогонялся | Дифф не касается rooms/wall-thickness/layout/marker.space/open_spans — furniture x/y/w/h/angle/flip_* decor-геометрия объекта, инварианты #254 к ней не адресуются (то же основание, что в r1) |
| performance | не прогонялся | Не назван в AC отдельно от bundle:budget; предрелизный гейт |
Прочитано заново (не только унаследовано из r1)
src/furniture.tsцеликом —resizeFurnitureTransform(crossing/flip-XOR логика, минимум 0.1 см, сохранение мирового anchor),furnitureRotationAngle(детерминированное округление половинного шага),furnitureRenderTransform(порядокrotate(...) translate(...) scale(...)— mirror применяется в локальном боксе до поворота, как того требует ТЗ §6).src/houseplan-editor-runtime.ts: furniture-ветка_dtMove(resize и rotate),_decorApplyFurnitureBox,_decorSaveShapefurniture-путь (блокировка Save приfurnitureWcm===null),_decorFurnitureSizeInput/_decorFurnitureFlip(синхронизация знака и чекбокса).src/houseplan-card.ts:_renderTransformFrame(четыре средние ручки,dtfurnitureframe), furniture render path в_renderDecorLayer(видимый path + невидимыйdfurniturehitтолько приediting && tool==='select', ширина =strokeWidth + 20см).src/styles/plan.styles.ts:pointer-eventsвключается только связкой.mode-decor.dtool-select, курсор поворота — локальный data-URI, без сети.custom_components/houseplan/validation.py/import_export.py: bool-схема, plan-only allowlist._dtUp(общий, не furniture-специфичный) — один history-commit на жест независимо отkind, ровно как требует AC9.- Тесты:
test/furniture.test.mjs— численные ассерты наresizeFurnitureTransform/furnitureRotationAngle(не строковые совпадения), способные упасть на смене знака/условия; аналогичноtests_backend/test_validation.py(0/1/"true"/None/[]/{}отклоняются как flags).
Всё перечисленное соответствует контракту ТЗ #383 и описанию из r1 — расхождений не найдено.
Критерии приёмки — построчно (обновлённый статус r2)
| AC | Доказательство | Итог r2 |
|---|---|---|
| AC1 угловой resize | unit (числовые ассерты) + smoke (cornerGrowsTheWidth, oppositeCornerStaysPut, defaultResizePreservesRatio и др., все true) |
PASS |
| AC2 одноосевые ручки | unit (edge-handle) + smoke (furnitureFrameHasFourSideHandles, edgeHandleChangesOneAxisContinuously) + golden-matrix статический контракт (44/44) + новый DOM-ассерт в demo/golden/harness.mjs (9/4 хендла) |
PASS (пиксели — предрелизно) |
| AC3 crossing/минимум | unit (crossed, both) + smoke (crossingTogglesOnlyHorizontalFlip, crossingCancelRestoresTheObject) |
PASS |
| AC4 rotation | unit (furnitureRotationAngle таблица) + smoke (rotationIsFreeWithoutShift, shiftSnapsRotationTo45) + прочитан _dtMove: rect/ellipse/text путь не тронут |
PASS |
| AC5 properties sync | unit (furnitureSignedFieldCm/Value) + smoke (negativeWidthSynchronisesHorizontalCheckbox, zeroSizeBlocksSave) + ?disabled=${invalidFurnitureSize} прочитан в коде |
PASS |
| AC6 render parity | furnitureRenderTransform прочитан построчно (mirror внутри local box, затем rotate вокруг центра — соответствует §6); golden-сценарии furniture-transform-light/dark добавлены (4 ориентации, light/dark, angle 0°/30°); пиксельная сверка — предрелизно |
PASS по коду; пиксели не подтверждены в этом раунде (то же основание, что в r1) |
| AC7 hit-area | код: смещение strokeWidth + decorCmToUnits(20,…), тот же furnitureStrokePx helper; CSS: pointer-events только в .mode-decor.dtool-select; smoke selectionHaloUsesTheRealArtworkPath |
PASS |
| AC8 cursor/frame | CSS прочитан: локальный data-URI, grab/grabbing fallback, .dt-ew/.dt-ns для средних ручек; smoke DOM-часть PASS; manual screenshot не выполнялся (как и в r1 — вне цикла ревью) |
DOM PASS; визуальный проход — вне этого цикла |
| AC9 история/cancel | _dtUp прочитан: не ветвится по kind, один commit на d.moved; smoke crossingCancelRestoresTheObject |
PASS |
| AC10 backend/transfer | test_validation.py (142 passed локально) — bool строго типизирован; test_ha_import_export.py прочитан (plan-only round-trip flip_h=True/flip_v=False сохраняется); HA-модули не исполнялись (ограничение среды) |
PASS (частично — чтением для HA-зависимых файлов) |
| AC11 регрессии | 11/12 прямых smoke зелёные (см. таблицу гейтов); общий resizeDecorBox/rect/ellipse/text путь в дифф не входит (сверено диффом _dtMove) |
PASS |
| AC12 i18n/docs/release | en/ru/de/fr парны (decor.flip_h/v, сверено grep по всем 4 файлам); FURNITURE.md, USER-GUIDE(.ru).md, CONFIG-COMPATIBILITY.md и др. читаемы и терминологически согласованы; скриншоты пересняты и приняты (b1b1e743, M1 закрыт); оба changelog в одном коммите с кодом (686557c5) |
PASS (M1 закрыт, было единственное препятствие) |
| AC13 гейты/бюджет | typecheck/test/build/bundle:budget/no-new-any — все зелёные, прогнаны лично в этом раунде; check-docs зелёный (CI + локально); golden/performance — предрелизно, как и заявлено | PASS |
Находки
Нет ни одной новой находки. M1 (единственная блокирующая находка r1) закрыт и подтверждён независимо. Low из r1 («143 vs 142 passed» — расхождение в формулировке хендоффа) не блокировал и не требует повторной проверки — природа не изменилась.
Что проверено и корректно
- M1 закрыт полностью по правилам §8: снята джоба
Docs screenshots(workflow_dispatch, run33302882691), артефакт принят точной командойnpm run docs:accept -- --reviewed --from=..., коммитb1b1e743содержит толькоdocs/images/**, трейлеры (Issue: #383,User-Visible: no) корректны.check-docs.mjsзелёный и локально, и в CIpreflight. - Ребейз на новый
devне создал скрытого взаимодействия с #385 (223f499e,e561e5fc) — разные функции, разные строки, подтверждено построчным чтением обоих коммитов. - Дешёвые и целевые гейты, прогнанные лично в этом раунде, зелёные:
typecheck, 1604/1604 unit, build+bundle:sync (3 копии байт-в-байт),
bundle:budget (запас 20853 B), no-new-any (0 новых), check-docs, целевой
backend-субнабор (142/143), целевой smoke (
furniture, все ключи true), 11/12 сопутствующих прямых smoke (единственный красный под-чек — воспроизводимое ограничение песочницы, не регрессия). - Furniture-only ветвление не задевает rect/ellipse/text/backdrop путь —
сверено чтением
_dtMove/_renderTransformFrame/_decorSaveShape. - «Одно число — один источник»:
test/single-source-numbers.test.mjsв зелёномnpm test; сигнатурные размерные поля в диалоге свойств — не повторный дисплей той же величины, а отдельная редактируемая проекция (коллизии нет, как и отметил r1).
Чего не проверял
- Пиксельные golden (AC2/AC6) и браузерный performance-профиль —
предрелизный гейт по §8/§11.4, не гейт этого ревью; статические контракты
сценариев прогнаны через
npm testи новый DOM-ассерт вdemo/golden/harness.mjs. - Manual screenshot для AC8 — вне цикла ревью по определению процесса.
- HA-зависимые backend-тесты (
test_ha_import_export.py,test_coordinate_canonicalization.py, полныйpytest tests_backend) —homeassistantнедоступен в этой песочнице (нет.venv-backend); разобраны чтением диффа, воспроизведено, что тот же импорт падает и на немодифицированном коде. - 30 слабых smoke-связей из
smoke-select— не прогонялись; список и причина отклонения идентичны r1 (широкие общие символы_curSpaceCfg,_decorTool, не относящиеся к furniture-специфичному коду). - model-invariants — дифф не касается geometry room/wall-thickness/
layout/marker.space/open_spans.
Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
|---|---|---|
M1 (Medium, в скоупе): check-docs.mjs красный — устаревший отпечаток скриншотов документации |
Снята джоба Docs screenshots (workflow_dispatch, run 33302882691), артефакт принят npm run docs:accept -- --reviewed --from=..., коммит b1b1e743 |
git show --stat b1b1e743 — только docs/images/**; node scripts/check-docs.mjs → PASS локально; CI preflight на b1b1e743 → success |
| L1 (Low, снята без правки r1): «143 passed» в хендоффе vs фактические 142 passed + 1 skipped | Не требовала правки — расхождение в формулировке, не в результате; в r2 автор сам указывает «143 passed» снова в комментарии после повторного rebase, я перепроверил напрямую: 142 passed + 1 skipped (143 собранных) | python3 -m pytest tests_backend/test_validation.py -q в этом раунде → 142 passed, 1 skipped |
Унаследовано из r1
Принято без повторного самостоятельного вывода (документ CODE-REVIEW-383-r1.md,
материал 8f34c594, содержимое исходников идентично текущему 686557c5 —
подтверждено сверкой списка файлов диффа и собственным построчным чтением
ключевых мест в этом раунде, а не слепым доверием):
- Разбивка AC → конкретные unit/smoke имена (какая проверка что доказывает) — взята из r1 как карта, но каждая PASS-отметка в таблице выше перепроверена заново прогоном в r2, а не скопирована.
- Мутационное рассуждение r1 («обратный знак
crossX/crossY, пропускkeepAspectломает конкретные ассерты») — не повторял мысленные мутации заново; вместо этого перечитал сам кодresizeFurnitureTransformи убедился, что численные тестыtest/furniture.test.mjsжёстко привязаны к конкретным входным/выходным числам (не строковые совпадения), что даёт ту же гарантию «тест умеет падать». - Заключение «прецедент
flip_h/flip_vу openings — не коллизия» (Low-2 из ревью ТЗ) — принято из r1 без повторной проверки схемы openings, так как сам факт документирован вCONFIG-COMPATIBILITY.md(сверено, что абзац присутствует) и openings/furniture проходят разными функциями allowlist (_DECOR_KIND_FIELDS), что видно и в текущем диффе.
Итог
Реализация полностью закрывает контракт ТЗ #383: все 13 AC доказаны
исполняемыми тестами (перепроверены лично в этом раунде, включая численные
таблицы, которые способны падать на смене знака/условия) либо разобраны
чтением с явной пометкой ограничения среды. Единственная находка r1 (M1)
закрыта и подтверждена независимо через CI и локальный повтор. Рёбейз на
ушедший вперёд dev не принёс скрытых конфликтов — оба привнесённых
коммита (#385) не пересекаются с кодом #383 по функциям/строкам. Дешёвые и
целевые гейты, прогнанные лично на этом SHA (а не унаследованные из
вводящего в заблуждение «зелёного» Validate — см. оговорку в разделе «Как
проверялось»), все зелёные. Новых находок нет.
Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0
Issue может переходить в S8-merged.