docs: review document for #82

Issue: #82
User-Visible: no
This commit is contained in:
claude[bot]
2026-08-30 16:11:36 +00:00
parent a015f31376
commit b2074c103c
+195
View File
@@ -0,0 +1,195 @@
# CODE-REVIEW-82-r2
- **Issue:** #82 «Плавное масштабирование плана: анимация zoom/fit/reset»
- **Этап:** code (PROCESS.md §2.7)
- **Заход:** r2 · блокирующих циклов израсходовано 0 из 4 (r1 был зелёным, бюджет не тратил, #227)
- **Материал:** `origin/dev...HEAD`, HEAD = `cc73252aa73007db3bd636b02eec3dd8ece871b2`
- **origin/dev на момент ревью:** `e39cd3dfa1453be140ab809cd4344f9474bd0b2c`
- **Диапазон:** 13 коммитов, `8a18e812`…`cc73252a`
- **Вердикт:** зелёный · High: 0 · Medium: 0
## Почему разбор полный, а не по дельте
После зелёного r1 (вердикт на SHA `779382af`, комментарий 2026-08-30T15:51:14Z)
ветка была ребейзнута на ушедший вперёд `dev` (`e39cd3df`), а после этого
довешено ещё 6 коммитов, которых r1 не видел (`a945ca47`…`b5324ce4`, см. ниже).
Ребейз на ушедший вперёд dev — один из явных случаев, для которых инструкция
требует полный разбор, а не разбор по дельте (§7.2: после ребейза это другой
код). Поэтому весь диапазон `origin/dev...HEAD` перечитан и перепроверен
заново, включая часть, которую r1 уже одобрил, а не только новые 6 коммитов.
Отдельное наблюдение по процессу (не находка к коду, Low): комментарий
владельца «Финальный rebase… Единственный конфликт был в
docs/images/screenshots.json… исходный код и поведение камеры не
конфликтовали» описывает состояние ДО того, как в ветку попали ещё 6 коммитов
(`a945ca47`…`b5324ce4`), один из которых (`64f54fd4`) меняет реальное поведение
`src/houseplan-card.ts` (см. AC1/AC5 ниже), а не только тесты и скриншоты.
Ни один из этих 6 коммитов не описан в отдельном комментарии issue. Это не
блокирует ревью — полный разбор ниже покрывает эти коммиты по существу — но
статус-комментарии в этом раунде не отражали фактический diff на момент
возврата задачи в S7.
## Что проверено и как
### Дешёвые гейты
Validate на точном HEAD `cc73252a` зелёный
(https://github.com/Matysh/houseplan-card/actions/runs/33320914612), поэтому
`tsc --noEmit`, `npm test` (1642, см. предыдущий зелёный прогон) и
`npm run build` со сверкой копий бандла не перегонялись как отдельная
проверка. Тем не менее `npm run bundle:sync` (= `tsc --noEmit && rollup -c` +
раскладка) выполнен локально по ходу подготовки к golden — прошёл чисто,
`dist`/`custom_components/.../frontend`/`demo/srv/assets` идентичны после
сборки, `git status` остался чистым (значит закоммиченные бандлы уже
соответствуют исходникам этого HEAD).
`node scripts/check-docs.mjs` — выполнен (diff трогает `src/**`):
`Documentation checks passed (7 files, 10 external links)`.
### Golden (не покрывается перечнем «дешёвых» гейтов из шапки этого раунда)
Diff меняет рендер (camera-контроллер, 8 «унаследованных из dev» golden-baseline
и 4 doc-скриншота, включая правку `screenshots.json`). Запущен
`npm run golden:verify` — **147/147 passed**, включая все 8 файлов из коммита
«test: accept inherited dev golden updates» (`device-dialog-desktop-de`,
`device-dialog-mobile-ru`, `toggle-entity-dialog-mobile-ru`,
`device-ripple-color-popover-mobile-ru`, `tray-medium-group-en` и т.д. — все
`passed` в выводе). Это подтверждает утверждение коммита не как заявление
автора, а как воспроизведённый прогон: текущий код рендерит ровно то, что
принято в качестве эталона, независимо от того, что сами PNG побайтово
отличаются от версии в `origin/dev` (шумовая перекодировка/иной прогон
захвата — не дефект).
### Browser smoke — выбор по диффу
`node scripts/smoke-select.mjs --base origin/dev --head HEAD`: 49 прямых
совпадений, 33 слабые связи (общий символ вроде `_modeTransitionBusy`,
`stopPropagation`). Прогнаны:
- все смок-файлы, которые сам diff **редактирует** (значит, r1 их в этом виде
не видел): `smoke_canvas_frame`, `smoke_infinite_canvas`, `smoke_kiosk`,
`smoke_kiosk_pan_lock` — все правки только добавляют `await` на
устаканивание камеры перед проверкой существующих инвариантов, поведение не
меняют;
- главный доказатель AC — `smoke_smooth_zoom` (250 строк, покрывает
AC1–AC12, AC14 согласно ТЗ §18);
- обязательный минимум регрессий из ТЗ §18 и уже проверенный r1 набор:
`smoke_zoom_out`, `smoke_editor_gestures`, `smoke_furniture`,
`smoke_opening_preview`, `smoke_visual_continuity` (#73),
`smoke_warm_remount`, `smoke_mode_transition` (#101),
`smoke_isometric_contract`, `smoke_glow`, `smoke_glow_geometry_resilience`.
Все 15 — **зелёные** (полный вывод каждого — все ключи `true`/`OK`).
Остальные 34 «прямых» совпадения из смок-select не прогнаны: они привязаны к
`_baseVb`/`_viewOr`/`_cursorPt`/`_modeTransitionBusy` — символам, которые
внутри строки почти не меняли поведение (только сигнатуры точек входа и
уборка opening-курсора при старте команды камеры, без изменения самой логики
черчения/декора/wall-thickness). Слабые связи (33) не прогонялись — общее имя
без прямого попадания в изменённый символ.
### Что не прогонялось и почему
- `python -m pytest tests_backend` — diff не трогает `custom_components/**/*.py`
(только сгенерированный JS-бандл внутри `custom_components/.../frontend`).
- `npm run invariants` — diff не трогает геометрию (нет правок рёбер комнат,
записей толщины, `layout`, `marker.space`, `open_spans`); камера работает
только с `viewBox`/`zoom`.
- Performance-профиль/screencast для AC13 — не прогонялся. ТЗ §21 и матрица
AC13 сами относят это к предрелизному Full Performance гейту, не к
автотесту в цикле ревью; то же решение уже дважды принято (r1 spec-review и
r1 code-review). Косвенно AC13-риск (Glow/geometry rebuild на кадре)
проверен `glowAndStructuralFrameStayStable` в `smoke_smooth_zoom` и
`smoke_glow_geometry_resilience` — оба зелёные.
- `test/single-source-numbers.test.mjs` — механически входит в `npm test`
(уже зелёный по Validate). Смысловая проверка сделана вручную: zoom badge
(`src/houseplan-card.ts:11517`, `Math.round(this._zoom * 100)}%`) — единственное
место, где процент зума выводится пользователю; он читает `this._zoom`
напрямую, то же поле, которое `_applyCameraTransitionFrame` обновляет на
каждом кадре. Второго источника числа не найдено.
## AC1–AC14: доказательства
| AC | Статус | Доказательство |
| --- | --- | --- |
| AC1 | Выполнен | `smoke_smooth_zoom`: `buttonHasIntermediateFrame`, `fitHomeAndDoubleTapAnimate`, `farFitUsesTheSameTransition` — все `true`; код `_stepZoom`/`_resetZoom`/`_fitAll`/`_fitFar` в `src/houseplan-card.ts:6270-6345` строят target существующей `fitView`/`_clampView` и передают его в `CameraTransitionController` |
| AC2 | Выполнен | `smoke_smooth_zoom.pinchIsDirectWithoutPostAnimation`, `smoke_editor_gestures` (pinch/pan в редакторе) — зелёные; `_zoomAt()` (immediate-путь) не создаёт transition |
| AC3 | Выполнен | Прочитано кодом: единственные reactive `_zoom`/`_view` пишутся только в `_applyCameraTransitionFrame`/`_settleCameraTransition`/`_zoomAt`, все SVG/HTML/hit-слои читают их через общий рендер-путь; `smoke_smooth_zoom.pointerdownFreezesPresentedFrame` подтверждает, что pointerdown видит реально представленный кадр, а не устаревший target |
| AC4 | Выполнен | `smoke_smooth_zoom.wheelRetargetsRunningTween` (разные token, `reverseFrom≈presentedBeforeReverse`), `wheelReversalKeepsAnchor` (≤0.5px) |
| AC5 | Выполнен | `smoke_smooth_zoom.exactLimitsAndFitAreNoops` (`active=false`, `saves=0` на fit/max/min); код `_startCameraTransition` (houseplan-card.ts:1207-1223) возвращает `false` без RAF/`_saveZoom`, если target совпадает с текущим или с уже бегущим target |
| AC6 | Выполнен | `pointerdownFreezesPresentedFrame`, `projectionCancelsAndIsoSettlesExactly`, `hiddenCommitsTargetOnce` — все `true`; код: `_cancelCameraTransition` вызван в `_stagePointerDown`, `_commitSpace`, `_setProjection`-пути (через `_cancelModeTransition`/adoption), `_refitView`, `_pageVisibility('hidden')`, config/layout adoption (`b3abfc67`) |
| AC7 | Выполнен | `smoke_warm_remount`, `smoke_visual_continuity` (#73) — зелёные без изменений поведения |
| AC8 | Выполнен | `reducedMotionIsImmediate` (`true`); `_onMotionChange` вызывает `_cancelCameraTransition(true)` при включении reduced motion, `_startCameraTransition` передаёт `duration=0` при `_reducedMotion` |
| AC9 | Выполнен | `buttonPersistsOnce`/`wheelStreamPersistsOnce` (`saves===1`); `_saveZoom()` вызывается только из `_settleCameraTransition`, один раз на settle |
| AC10 | Выполнен | `projectionCancelsAndIsoSettlesExactly` — итоговый `viewBox`/`zoom` точно совпадает с независимо вычисленным `_cameraTargetAt` |
| AC11 | Выполнен | `glowAndStructuralFrameStayStable` (`JSON.stringify` до/после равны); `smoke_glow`, `smoke_glow_geometry_resilience` зелёные |
| AC12 | Выполнен | `disconnectedCallback` вызывает `this._cameraTransition.dispose()` (houseplan-card.ts:2745); unit `viewport-transition.test.mjs` — `dispose()`→`cancel(false)` не оставляет RAF (`fake.callbacks.size===0` в тесте «cancel policies») |
| AC13 | Deferred (не автотест здесь) | То же решение, что в r1 spec- и code-review: закрывается предрелизным Full Performance/screencast гейтом, не циклом ревью. Косвенно риск закрыт `glowAndStructuralFrameStayStable` |
| AC14 | Выполнен | `coldViewDoesNotLoadEditorRuntime` (`true`, проверка через `performance.getEntriesByType('resource')`); `src/houseplan-editor-runtime.ts` не тронут диапазоном |
## Дельта сверх того, что видел r1
Сверх `c465eae8`/`54403704`/`b3abfc67` (уже одобрены r1 под именами
`895b120e…779382af`) в диапазон добавлено:
- `a945ca47` — `demo/docs/capture.mjs`: ждёт `card._cameraTransition?.active`
перед снимком, иначе бросает. Тестовая инфраструктура, поведение продукта
не меняет.
- `64f54fd4` — **единственный коммит с правкой продуктового кода** сверх
того, что видел r1: `_fitFar()`/`_fitAll()` получили безусловный
`this.requestUpdate()` перед `_resetZoom(...)` (houseplan-card.ts:5975,
5985). Причина: `_showFar` — обычное приватное поле, не Lit `@state()`
(houseplan-card.ts:5755), поэтому раньше рендер после клика по far-hint
зависел от того, что `_resetZoom()` синхронно меняла `_zoom`/`_view` и тем
самым просила update. С #82 `_startCameraTransition` может вернуть `false`
без единого вызова хуков (когда camera-target уже совпадает с текущим) —
и тогда без явного `requestUpdate()` смена `_showFar` не попала бы в
рендер. Остальные правки того же коммита — `await` на устаканивание камеры
в `demo/golden/harness.mjs`, `smoke_canvas_frame`, `smoke_infinite_canvas`,
`smoke_kiosk` — тестовая инфраструктура.
**Low, не блокирует:** конкретно этот no-op-путь (far-hint нажат, когда
camera-target уже равен текущему — например повторный клик до истечения
предыдущего fit-far) не покрыт отдельной smoke-проверкой; существующие
тесты far-hint (`smoke_infinite_canvas.hintGoneAfterShow`) всегда бьют по
сценарию с реальным движением камеры. Правка защитная и дешёвая
(лишний `requestUpdate()` не создаёт видимых артефактов), риск регрессии
низкий, поэтому не блокирует зелёный вердикт.
- `4c4399a7`, `4e24e266`, `9171644f` — три последовательных пересъёма
doc-скриншотов (`08-room-card.png`, `09-device-info.png`,
`06-device-editor.png`) с обновлением `screenshots.json`; `check-docs.mjs`
зелёный на финальном состоянии.
- `b5324ce4` — 8 golden-baseline из `origin/dev`, подтверждены прогоном
golden:verify выше (не только заявлением коммита).
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где это видно |
| --- | --- | --- |
| `55cf36d6`/`b3abfc67 fix: preserve structural adoption guard` — по факту перестановка двух независимых операторов без наблюдаемой разницы поведения, замечание только к формулировке сообщения коммита | Не блокировала r1, не блокирует и сейчас; независимо перечитан diff коммита `b3abfc67` в этом раунде — подтверждаю: переставлен только порядок `this._cancelCameraTransition(false)` относительно очистки `_geometryHistory`/`_devicePositionHistory`/`_pendingPhysicalWrites`, эти операции не пересекаются по состоянию | `git show b3abfc67 -- src/houseplan-card.ts` |
| AC13 закрыт только предрелizным Full Performance гейтом, не автотестом в цикле | Тот же прецедент подтверждён и на этом SHA — решение не изменилось | Раздел AC13 выше |
## Унаследовано из r1
Поскольку ребейз на ушедший вперёд `dev` требует полного разбора (§7.2), из
r1 ничего не принято «на слово» без собственной перепроверки в этом
раунде — весь диапазон, включая уже одобренные r1 коммиты, перечитан заново
и его гейты (golden, smoke, check-docs, build) перезапущены в этом раунде.
Без повторной проверки принято только решение уровня продукта/процесса,
которое не является предметом код-ревью:
- продуктовая корректность ТЗ (структура §7.1, формулировки AC, отсутствие
недекларированных догадок) — из `SPEC-REVIEW-82-r1.md`
(`docs/reviews/SPEC-REVIEW-82-r1.md`, ТЗ на SHA `141d79a9`); код-ревью не
переоценивает качество ТЗ, только соответствие ему;
- прецедент «AC13 закрывается предрелизным Full Performance гейтом» — принят
ещё в `SPEC-REVIEW-82-r1.md` и переподтверждён `CODE-REVIEW-82-r1.md`; в
этом раунде не пересматривается заново, только сверено, что новых
Glow/geometry-путей в дельте нет (см. выше).
## Итог
High: 0. Medium: 0. Low: 3 (2 унаследованы и переподтверждены, 1 новая —
непокрытый smoke-тестом no-op-путь `_fitFar()`/`_fitAll()`, не блокирует).
Все AC1–AC12, AC14 подтверждены исполненным автотестом или разобраны по коду
с указанием строк; AC13 корректно отложен на предрелизный гейт по уже
принятому прецеденту. Вердикт: зелёный.