Files
houseplan-card/docs/reviews/CODE-REVIEW-361-r2.md
T
2026-08-29 07:55:23 +00:00

205 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-361-r2
Issue: [#361](https://github.com/Matysh/houseplan-card/issues/361) — «Мебель: физическая толщина линий не масштабируется при zoom»
Этап: code · заход r2 · блокирующих циклов израсходовано 1/4 (зелёные вердикты бюджет не тратят, #227)
ТЗ: `docs/specs/361-furniture-stroke-zoom.md` (зелёный в SPEC-REVIEW-361-r2, SHA `31c6e77c`)
Раунд r1 код-ревью: жёлтый, SHA `4b273e94` (документ `docs/reviews/CODE-REVIEW-361-r1.md`,
единственная Medium-находка: golden-бейзлайны с реальной мебелью не приняты под новый
физический stroke).
Ветка приведена к `dev` конвейером до этого раунда: поверх легло 3 dev-коммита
(`8896d610 -> 737d2f66`, добавлены `c50d9e42`, `631bafea`, `00197a3a`, `23d6681d`,
`cbf9318e`, `f2e36572`, `7c45372c` — параллельные задачи #369/#366/#39, не #361). По §7.2
это другой код, и разбор ниже — **полный**, а не по дельте, несмотря на то что это
формально второй код-ревью-цикл.
## Скоуп проверки
Диапазон `origin/dev...HEAD`, 46 файлов. Продуктовый код — тот же, что уже был полностью
разобран в r1: `src/furniture.ts` (+55/-4), `src/houseplan-card.ts` (+30/-6), коммит
`6c608485` (`fix: scale furniture strokes with plan zoom`, trailers `Issue: #361` /
`User-Visible: yes`). Я построчно сверил оба файла с диффом, процитированным в
`CODE-REVIEW-361-r1.md`, — содержимое побайтово идентично тому, что уже прошло полный
разбор на `4b273e94`; сменился только SHA из-за двух последовательных ребейзов (сначала
ручной автором на `dev@23d6681d` после конфликта в generated bundle, затем конвейерный
перед этим ревью). Остальное — тесты, три копии бандла, документация, changelog,
docs-скриншоты и golden-бейзлайны.
Продуктовое рассуждение (сверено заново, не по памяти r1): задача закрывает строку **J6**
`docs/SCOPE.md` («Keep the plan true as the home evolves» — редактирование/drag/resize
существующих элементов плана должно оставаться физически достоверным) и косвенно **J4**
(создание плана «как физического объекта»). Диагноз (line/rect/ellipse получают физический
`stroke-width` без `vector-effect`, мебель — тот же `stroke-width`, но с принудительным
`non-scaling-stroke`, который одновременно гасит и локальный анизотропный transform, и
внешний camera zoom) подтверждён чтением кода заново, не переносится как готовый вывод.
Зелёного Validate на `737d2f66` нет — все гейты ниже прогнаны мной локально.
## Как проверялось
1. Прочитаны заново: весь diff `src/furniture.ts`/`src/houseplan-card.ts`, ТЗ, issue и все
комментарии обоих SPEC-REVIEW и обоих CODE-REVIEW раундов, `docs/SCOPE.md` (J4/J6),
`docs/FURNITURE.md`, `docs/USER-GUIDE.ru.md`.
2. Построчное сравнение текущего диффа `src/furniture.ts`/`src/houseplan-card.ts` против
origin/dev с кодом, процитированным в `CODE-REVIEW-361-r1.md`: `furniturePlanScreenScale`,
`furnitureStrokePx`, точки вызова в `_renderFurniturePlacementPreview` и
`_renderDecorLayer` — идентичны знак в знак.
3. Дешёвые гейты (прогнаны сам, Validate на этом SHA не найден):
- `npx tsc --noEmit` — **PASS**;
- `npm test` — **1533 pass, 1 skip, 0 fail** (~26.5s, полный набор);
- `npm run build` — чисто, `git status --short` пуст после сборки;
- `npm run bundle:sync` — три копии бандла (`dist/`, `custom_components/houseplan/frontend`,
`demo/srv/assets`) пересобраны, `git status --short` после — пуст, дифф с рабочим
деревом нулевой;
- `npm run bundle:budget` — **PASS**, initial View 273551 B gzip / budget 282000 B,
запас 8449 B (запас чуть меньше, чем в r1 (9152 B), — за счёт трёх посторонних dev-
коммитов, легших поверх при ребейзе, не этой задачи);
- `node scripts/check-docs.mjs` (diff трогает `src/**`) — **PASS**, 7 файлов, 10 внешних
ссылок;
- `node scripts/no-new-any.mjs --base origin/dev --head HEAD` — **PASS**, 75 новых строк
в 2 файлах, новых `any` нет.
4. `node scripts/smoke-select.mjs --base origin/dev --head HEAD` — та же картина, что в r1:
29 «прямых совпадений» по `_baseVb`/`_viewOr`/`_stageEl` (методы камеры/viewport, не
изменены диффом, только читаются в новой ветке рендера мебели) и 1 «слабая связь»
(`smoke_canvas_frame.mjs` ← `_baseVb`). Решение по каждой строке — как в r1: методы не
изменены, ни pan/zoom/backdrop/kiosk/жесты/виртуальный свет к furniture-stroke отношения
не имеют — не прогонялись; `smoke_canvas_frame.mjs` (слабая связь) просмотрен — про
рамку вида/aspect, мебели не касается, не прогонялся.
5. Целевой смок из AC1/AC3/AC4/AC6: `node demo/smoke_furniture.mjs` → **OK**, все 90 полей
`true`, включая растровые (`furnitureFollowsPhysicalCameraZoom`,
`designerAndPrimitiveMatchOrdinaryDecor`, `anisotropicResizeKeepsBothAxesEqual`,
`rotatedArtworkKeepsTheSameThickness`, `viewportResizeRecalculatesTheSharedPhysicalStroke`,
`previewAndCommitShareThePhysicalStroke`).
6. **Дисциплина «тест должен уметь падать» — унаследована из r1, не переисполнена**: revert-
проверка (откат `src/furniture.ts`/`src/houseplan-card.ts` к `origin/dev`, повторный
прогон `furniture-stroke-contract.test.mjs` и `demo/smoke_furniture.mjs`, восстановление
дерева) уже документирована в `CODE-REVIEW-361-r1.md` на побайтово идентичном коде.
Гонять заново дорогой Chromium-revert-цикл ради кода, который я построчно сверил как
идентичный, — трата времени без нового знания; см. раздел «Унаследовано из r1».
7. Golden baseline — находка r1. Проверил закрытие на факт-коде:
- `git diff origin/dev...HEAD -- demo/golden/baselines/baselines-index.json` показывает,
что изменены ровно 4 записи из ТЗ (`furniture-placement-preview-light`,
`furniture-plan-art-dark`, `decor-over-opaque-hover-light`,
`decor-over-glow-base-dark`) плюс `sourceFingerprint`/`acceptedAt`/`witnesses.count`
(133→131, floor 10 — запас есть). Остальные 138 хешей не тронуты;
- после `npm run bundle:sync` (чтобы `demo/srv/assets` был собран из факт-кода) прогнал
`node demo/golden/run.mjs --mode=capture --scenario=<id>` для всех 4 сценариев →
**`passed` на всех четырёх** (актуальный рендер совпадает с новым принятым бейзлайном);
- контрольный прогон `furniture-categories-light` (мебель есть только в палитре,
decor-слоя с реальной мебелью нет) дал `different` (`diffRatio≈0.00175` при пороге
`0.0008`); чтобы отличить реальную регрессию от дрейфа среды, откатил
`src/furniture.ts`/`src/houseplan-card.ts` к `origin/dev`, пересобрал (`bundle:sync`) и
повторил тот же сценарий — **тот же `actualSha256`, тот же `diffRatio`**, то есть
расхождение существует независимо от этого фикса (предсуществующий дрейф рендеринга,
не тронутый диффом) — не находка. После проверки восстановил рабочее дерево (`cp`
оригиналов + `git reset HEAD`, `git diff HEAD` пуст) и пересобрал бандл;
- `npm run golden:verify` целиком (182 сценария) не запускался: инструмент сам запрещает
`--scenario` в режиме `verify` («golden verify must run the complete matrix»), а diff не
трогает ничего вне decor/furniture render — остальные сценарии не имеют мебели в
decor-слое и не могут пострадать от этого diff (то же обоснование, что в r1).
8. `npm run invariants` не прогонялся: `git diff origin/dev...HEAD --stat -- src/` показывает
только `src/furniture.ts` и `src/houseplan-card.ts`, ни один не пишет и не читает
`WallSegmentModel`/рёбра комнат/`layout`/`marker.space`/`open_spans` — только
decor/furniture render (перепроверено чтением обеих новых функций и их точек вызова).
9. `python -m pytest tests_backend` не прогонялся: diff не трогает `custom_components/**/*.py`
(только сгенерированные frontend-бандлы внутри `custom_components/houseplan/frontend/`).
10. Performance-профили не прогонялись: AC8 не называет конкретный профиль; source-contract
тест (`test/furniture-stroke-contract.test.mjs`) мутационно гарантирует, что
`furnitureScreenScale` резолвится один раз на весь decor-слой, не на предмет.
11. «Одно число — один источник»: физическая толщина мебели вычисляется один раз
(`furnitureScreenScale` в `_renderDecorLayer`) и передаётся параметром в
`_renderFurniturePlacementPreview` — единственная точка получения
`furnitureStrokePx(...)` для preview и единственная для saved-пути; второго независимого
вычисления той же видимой величины нет (проверено чтением: `houseplan-card.ts:8095-8098`,
`8171`, и мутационно — `furniture-stroke-contract.test.mjs` требует ровно одно вхождение
`const furnitureScreenScale = furniturePlanScreenScale(`). `test/single-source-numbers.test.mjs`
эту величину не называет явно, но входит в зелёный `npm test` и не задет диффом.
## Закрытие раунда r1
| Находка r1 (Medium, в скоупе) | Чем закрыта | Где это видно |
|---|---|---|
| Golden-бейзлайны 4 сценариев с реальной мебелью (`furniture-plan-art-dark`, `furniture-placement-preview-light`, `decor-over-opaque-hover-light`, `decor-over-glow-base-dark`) не обновлены под новый физический stroke; `npm run golden:verify` упал бы на SHA `4b273e94`. | Коммит `737d2f66` (`test: accept furniture stroke golden baselines`, trailers `Issue: #361` / `User-Visible: no` / `Baseline-Reviewed: <CI run 33240556186>`) принимает ровно эти 4 хеша через `npm run golden:accept -- --reviewed` из полного Linux CI-артефакта; остальные 138 бейзлайнов не тронуты. | `git diff origin/dev...HEAD -- demo/golden/baselines/baselines-index.json` — 4 изменённых записи; `node demo/golden/run.mjs --mode=capture --scenario=<id>` → `passed` на всех четырёх на факт-коде (раздел «Как проверялось», п.7). |
Других находок r1 не было (High: 0, Medium: 1 — закрыта выше).
## Унаследовано из r1
Со ссылкой на `docs/reviews/CODE-REVIEW-361-r1.md`, SHA `4b273e94`:
- **Falsifiability-проверка новых тестов и смока** (revert `src/furniture.ts`/
`src/houseplan-card.ts` к `origin/dev`, повторный прогон
`test/furniture-stroke-contract.test.mjs` и `demo/smoke_furniture.mjs`, наблюдение падения
ровно на ожидаемых утверждениях, восстановление дерева) — не переисполнена в этом раунде.
Основание: я построчно сверил текущий `src/furniture.ts`/`src/houseplan-card.ts` с кодом,
процитированным в r1, — содержимое идентично; повторный дорогой Chromium-revert-цикл на
неизменившемся коде не добавляет знания.
- **Разбор нетипичного iso-случая** (`_baseVb()` → `!showBorders`/no-borders ветка, где
`furnitureScreenScale` считается от `planView`, а фактический viewBox decor-слоя иногда
берётся из `floorView`) — переперечитан мной в этом раунде (те же строки
`houseplan-card.ts:11087-11090`, тот же вывод: оба входа конечны и положительны,
`NaN`/пропажа мебели невозможны, численная калибровка iso вне контракта #361) —
формально не «унаследовано без проверки», а подтверждено заново, привожу здесь для
полноты трассировки.
- **Продуктовый диагноз и AC1…AC9 как таковые** (что чинить, что не входит в скоуп) —
установлены на этапе SPEC-REVIEW-361-r2 (SHA `31c6e77c`, зелёный); в этом раунде я не
оспаривал сам контракт, а проверял, что реализация ему соответствует.
## Проверено и корректно (по AC)
- **AC1/AC3 (физический zoom, rotate+zoom):** `furniturePlanScreenScale` — точная формула
ТЗ `min(viewportW/viewBoxW, viewportH/viewBoxH)`, учитывает letterboxing. Растровый смок
подтверждает кратное изменение толщины при переходе zoom1→zoom2 для designer- и primitive-
артворка, включая повёрнутый на 30°.
- **AC2 (анизотропный resize):** `furnitureStrokePx` не принимает `w`/`h`/`viewW`/`viewH`
предмета вообще — анизотропии неоткуда взяться; смок подтверждает равенство толщины по
осям в пределах допуска.
- **AC4 (preview/commit parity):** один расчёт `furnitureScreenScale` на весь decor-слой,
передан параметром в preview — единый источник для обоих render-путей; source-contract
тест запрещает preview обходить `furnitureStrokePx`; смок
`previewAndCommitShareThePhysicalStroke: true`.
- **AC5 (совместимость):** полный `npm test` зелёный (1533/1533, все 56 symbols, erase-hit,
`data-hp`/`data-kind`/`data-symbol` не задеты); смок целиком (палитра, drag, resize,
rotate, wall magnet, erase) зелёный.
- **AC6 (layout safety):** юнит-тесты на нулевые/`NaN`/`Infinity` входы всегда возвращают
конечное число; смок `viewportResizeRecalculatesTheSharedPhysicalStroke` подтверждает
пересчёт после ресайза viewport.
- **AC7 (поддерживаемые поверхности), кроме golden:** рендер общий для View/kiosk/
Background editor, light/dark не входят в расчёт (проверено чтением); golden — закрыт
выше отдельным разделом. Один нетиповой iso-случай разобран как явно принятый остаточный
риск (раздел «Унаследовано из r1»), не находка.
- **AC8 (perf/bundle):** typecheck/test/build/bundle:sync/bundle:budget зелёные;
`furnitureScreenScale` — один вызов на слой, гарантировано мутационным тестом.
- **AC9 (документация):** `docs/FURNITURE.md` больше не приписывает `non-scaling-stroke`
физическую толщину; оба changelog обновлены в том же коммите `6c608485`
(`User-Visible: yes`); `docs/USER-GUIDE.ru.md` — одно согласованное с разделом предложение.
## Чего не проверял
- Полный `npm run golden:verify` (182 сценария) — режим `verify` не допускает фильтр по
сценарию, а прогнать все 182 ради diff, трогающего только decor/furniture render, —
несоразмерная трата времени; прогнаны 4 названных в находке r1 сценария плюс 2 контрольных
(`furniture-categories-light`, `tray-narrow-tool-ru`), с явным разбором «различие или
дрейф среды» для контроля.
- Falsifiability revert-check новых тестов/смока — не переисполнена, см. «Унаследовано из
r1» с обоснованием.
- Полная browser-smoke матрица (201 файл) — прогнан целевой `smoke_furniture.mjs`; связи,
названные `scripts/smoke-select.mjs`, разобраны построчно (раздел «Как проверялось», п.4).
- `npm run invariants` — diff не геометрический (обоснование в п.8).
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py` (п.9).
- Performance-профили/бенчмарки — не названы в AC, риск закрыт чтением и мутационным тестом.
- Ручное тестирование в браузере человеком — не проводилось; весь цикл проверен автоматикой
и чтением кода, как предписано процессом для код-ревью.
## Вывод
Код и тесты в этом раунде — тот же, уже полностью разобранный в r1, продукт содержательно
не изменился, изменился только SHA из-за ребейза на ушедший вперёд `dev`. Единственная
Medium-находка r1 (непринятые golden-бейзлайны) закрыта точно предписанным в ТЗ способом:
4 названных сценария приняты из полного Linux CI-артефакта, `passed` подтверждён локальным
`--mode=capture` прогоном на факт-коде, контрольные сценарии показывают только
предсуществующий дрейф среды, не связанный с этим диффом. Новых находок нет.
**High: 0 · Medium: 0 → в задаче**