Files
houseplan-card/docs/reviews/CODE-REVIEW-531-r2.md
T
2026-09-11 11:55:24 +00:00

242 lines
23 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-531-r2
**Issue:** #531 «Панорама лагает в Firefox: viewBox переписывается каждый кадр»
**Материал:** ветка `issue/531-live-pan-transform`, SHA `77a3d2607ceed859d1244e4943c422306f34ec3e` (дерево `c5c45966e48b9737df030a2c69e6bffe2d9bfb40`), вершина `origin/dev` (`9d35ab35`)
**Заход:** r2 · блокирующих циклов израсходовано 1/4
**Вердикт: зелёный**
## Почему разбор полный, а не по дельте (§2.10, §7.2)
Между r1 и r2 ветку пришлось ребейзить на ушедший вперёд `dev`: после r1
`dev` принял #530 (печать плана), и слияние конфликтовало — конвейер сам
вернул задачу в `S6-in-progress` без траты цикла (комментарий 11:17:57Z),
автор сделал `git rebase origin/dev`, разрешил конфликт и вернул
`S7-code-review` (11:27:06Z). Это ровно случай из PROCESS.md §2.10: «ребейз
на ушедший вперёд `dev` — после ребейза это другой код», для которого
раздел прямо требует полный разбор, а не только находки.
Полный разбор ниже это подтверждает точечно: конфликт ребейза лежал
целиком в производных копиях (бандл-дерево, `docs/images/screenshots.json`,
построчное слияние обоих changelog) — ни один продуктовый файл `#531`
(`src/live-viewport.ts`, тесты, смок, мутанты, `docs/ARCHITECTURE.md`) в
конфликте не участвовал. SHA r1 (`57b80f08`) осиротел ребейзом — по корпусу
ревью (98 из 804) это обычное дело, а не находка (§2.10). Материал r1
недоступен для прямого `git diff` в этой рабочей копии (`git cat-file`
на `57b80f08` не резолвится, дерево `2b20dd807a08` не находится ни в одном
коммите текущего `git log --all`) — сверка сделана иначе: содержимое
`src/live-viewport.ts`, `test/live-viewport.test.mjs`,
`scripts/mutation-gate.mjs`, `docs/ARCHITECTURE.md` на `HEAD` построчно
сравнено с описанием в `docs/reviews/CODE-REVIEW-531-r1.md` (сохранён в
этой же ветке, коммит `af3be59a`) — совпадает без исключений, плюс единственное
добавление — новый юнит-тест изометрии из ответа автора на r1. `#530`
(`git show 2e26116b`) не касается ни одного файла в этом диапазоне и не
трогает `demo/golden/baselines/**` — интерференции с #531 по коду нет.
Вывод: рассматриваю задачу так, будто вижу её впервые (полная сверка
кода, гейтов и AC ниже), но фактическая находка нового объёма равна нулю
— ребейз ничего не изменил по существу.
## Скоуп
ТЗ (спор-ревью зелёный на r2, `docs/reviews/SPEC-REVIEW-531-r2.md`) требует:
во время жеста панорамы SVG-сцена двигается CSS-трансформом, а `viewBox`
переписывается не чаще, чем позволяет бюджет (`LIVE_VIEWBOX_REFRESH_MS`
≈100 мс либо `LIVE_VIEWBOX_REFRESH_SHIFT` ≈15 % сдвига/масштаба);
терминальное примирение не меняется; ничего не пишется в DOM, если строка
не изменилась; слой устройств и сцена не расходятся (#451). AC1–AC9 в теле
issue.
Диапазон диффа к `origin/dev` (после ребейза, `origin/dev...HEAD`, без
класса D): `src/live-viewport.ts` (единственный продуктовый файл),
`test/live-viewport.test.mjs` (новый + 1 тест из ответа на r1),
`demo/smoke_live_pan_viewbox.mjs` (новый), `scripts/mutation-gate.mjs`
(3 новые записи), `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md`,
`docs/ARCHITECTURE.md`, `docs/images/screenshots.json` (только отпечаток),
`docs/reviews/CODE-REVIEW-531-r1.md`, плюс три копии сгенерированного
бандла (класс D). `src/live-interaction-runtime.ts` не тронут — планировщик
жеста не менялся.
## Закрытие раунда r1
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| Medium: `node scripts/check-docs.mjs` красный — правка `src/live-viewport.ts` состарила отпечаток скриншотов, `docs/images/screenshots.json` не тронут | `npm run docs:accept -- --identical` (коммит `9d1ba110`, отпечаток обновлён вторично в `77a3d260` после ребейза — см. ниже) | `git show 77a3d260 -- docs/images/screenshots.json`: меняются только поля `sourceFingerprint`/`sourceSha256`, ни один `imageSha256` не тронут (0 расхождений пикселей); `node scripts/check-docs.mjs` на `HEAD` → `Documentation checks passed (7 files, 12 external links)`, проверено выполнением |
| Наблюдение без правки (не находка): риск «изометрия» из ТЗ не подтверждён тестом буквально — все прежние кадры используют `floor === view` | Автор добавил девятый юнит `#531 изометрия: камера и пол проецируются раздельно и оседают вместе` (коммит `9d1ba110`) | `test/live-viewport.test.mjs:151-171`; проверено выполнением — `node --test test/live-viewport.test.mjs` → 9/9, и мутацией руками (`sceneFloor = sceneCamera`) тест краснеет (`not ok 9`), после отката — снова 9/9 |
Особенность: правка, закрывшая Medium (обновление `docs/images/screenshots.json`),
после ребейза на `dev` с #530 сама устарела повторно — `dev` с #530 тоже
меняет `src/**`-зависимый отпечаток. Автор закрыл это вторым коммитом
(`77a3d260`, «Пересборка бандла и отпечатка доков после ребейза на dev»),
и на `HEAD` гейт зелёный — проверено выполнением, не по заявлению.
## Унаследовано из r1
Из документа `docs/reviews/CODE-REVIEW-531-r1.md` (материал `57b80f08`)
принято без повторного самостоятельного вывода, но с перепроверкой, что
код на `HEAD` совпадает построчно с тем, что там описано (см. раздел выше
«Почему разбор полный»):
- К1–К5 в коде соответствуют ТЗ (якорь `anchor`, `needsViewBoxRefresh`,
дедупликация записи в `setViewBox`/`setLayerProjection`);
- две базы проекции (сцена — от `anchor`, HTML-слой — от осевшего
Lit-кадра) — намеренная и корректная асимметрия, а не путаница;
- отклонение AC5 от буквального текста ТЗ («не больше двух» →
`max(2, ceil(elapsed/100)+1)` + `< frames/2`) принято ревьюером r1 как
корректное техническое решение (частота кадров песочницы отличается от
машины владельца) — технический спор автора и ревьюера, решённый
вердиктом (§7.2), повторно не открываю;
- таблица «чем краснеет» (#435) для AC1–AC9 — методология и мутанты
проверены r1 **выполнением** (патч применён руками, тест красился),
сам код мутантов не менялся (сверено байт в байт, см. ниже).
Что не наследую, а перепроверил заново в этом раунде (см. «Как
проверялось»): весь набор гейтов на актуальном `HEAD`, включая свежий
прогон юнитов, смока, golden и мутации на новом изометрическом тесте —
дельта ребейза формально обнуляет доверие к прежним числам, пока они не
подтверждены на новом SHA.
## Как проверялось
Дешёвые гейты подтверждены зелёным Validate ровно на этом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/34594144644,
`conclusion: success`, job «Фронтенд: типы, юниты, мутанты, синхрон
бандла» и все 6 job «Мутанты по диффу» — success; `smoke`/`golden`/
`performance_smoke`/`backend`/`hacs`/`hassfest` — `skipped`, обычный push
не «тяжёлый»). Дополнительно перегнал сам — часть проверок требует
пересобранного бандла и гейтов, которые Validate на обычном push не
гоняет:
| Гейт | Команда | Результат |
|---|---|---|
| Типы + юниты + сборка | `npm run build` (включает `tsc --noEmit`), `npm run bundle:sync` | green; пересборка байт-в-байт совпала с закоммиченным деревом (`git status --short` пуст после) |
| Юнит-тесты | `npm test` | 2528 passed, 1 skipped (HA-харнесс), 0 failed |
| Юниты `#531` отдельно | `node --test test/live-viewport.test.mjs` | 9/9 (было 8 на r1, +1 изометрия) |
| Бюджет бандла | `npm run bundle:budget` | initial View 287 227 Б gzip (потолок 288 300 ±2000) — предупреждение о запасе <15000 Б общепроектное (#367/#474), к #531 не относится |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | 109 добавленных строк в 1 файле, новых `any` нет |
| Документация фронтенда (diff трогает `src/**`) | `node scripts/check-docs.mjs` | green: «Documentation checks passed (7 files, 12 external links)» |
| Смок AC5/AC6 | `node demo/smoke_live_pan_viewbox.mjs` | green: `framesInGesture=20`, `viewBoxWritesDuringGesture=4`, `framesWithSceneTransform=16`, `worstParityPx=0` — совпадает с числами r1 и хендоффа |
| Golden (AC7) | `npm run golden:verify` | 166/169 passed, **3 different** — все три `pdf-export-*`, см. находку ниже; не относится к #531 |
| Мутанты (AC8) — независимое подтверждение | CI «Мутанты по диффу» 6/6 success на этом SHA + я лично воспроизвёл мутацию на новом изометрическом тесте (см. «Закрытие раунда r1») | мутант красит тест; после отката — снова зелёный |
| `smoke-select.mjs` | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | НЕОПРЕДЕЛЁННОСТЬ — новые символы (`needsViewBoxRefresh`, `LIVE_VIEWBOX_REFRESH_*`, `LiveViewportAnchor` и др.) не связаны ни с одним существующим смоком; свидетель для них — сам новый `demo/smoke_live_pan_viewbox.mjs`, названный в AC5/AC6. Широких совпадений — 0 |
| Провенанс | `git show -s --format=full` на всех 4 коммитах ветки | `Issue: #531` на каждом, `User-Visible: yes` на продуктовом коммите (`58e31668`), `no` на трёх служебных (документ ревью, фикс r1, пересборка после ребейза); ветка `issue/531-live-pan-transform` |
| Инварианты модели | не запускал | диф не трогает геометрию/`layout`/толщины/`marker.space` — не применимо |
| `pytest tests_backend` | не запускал | диф не трогает `custom_components/**/*.py` — не применимо |
| `performance_smoke` (`large-house-interaction-v1`) | не запускал | ТЗ прямо выводит миллисекунды из-под AC («Приёмка по скорости — отдельно и не здесь», единственный судья — профиль с машины владельца); диф формально подпадает под правило классификации (`src/live-*`), но Validate на этом SHA не гонял его по той же причине (job `heavy`-гейтится, push обычный, не кандидат/PR/nightly) — согласуется с r1 |
Рабочее дерево после всех прогонов и намеренной мутации (изометрический
тест, откачена) восстановлено ровно к `HEAD` (`git status --short` пуст).
## Находки
Блокирующих находок нет. Одно наблюдение — вне скоупа задачи, не
находка #531 и не повод для отдельного issue (уже задокументировано и
принято ранее):
### Golden: 3 кадра `pdf-export-*` отличаются — это долг #530, не #531
`npm run golden:verify` на `HEAD` даёт 166/169 вместо 169/169 (как было
на r1 до ребейза): `pdf-export-geometry-light`, `pdf-export-polish-light`,
`pdf-export-stepped-dimensions-light`. Проверил `artifacts/golden/diff/
pdf-export-geometry-light.png` — план заметно крупнее и без столбца
численных отметок, что дословно совпадает с описанием #530 («сцена
печатается на 1,75 крупнее плана, столбец с отметками убран»).
`src/live-viewport.ts` не пересекается с PDF-экспортом (`src/pdf/**`
диффом не тронут вообще), и ребейз не задел `demo/golden/baselines/**`.
Проверка по истории: `git log --oneline origin/dev -- demo/golden/
baselines` не содержит коммита #530 — эталоны для него не пересняты. Это
подтверждает и собственный ревью-комментарий #530 (владелец/автор,
11.09): «Golden: долг, который надо закрыть на кандидате беты… все
семантические контракты этих кадров проходят и до, и после —
расхождение чисто в пикселях, эталоны надо переснять» — то есть долг
уже назван, принят и явно отложен на предрелизный гейт (PROCESS.md §8:
«полные наборы — предрелизный гейт, а не гейт ревью»; «Верифицирующий
и капчурящий — разные вещи», принятие эталонов не по каждой задаче).
Вывод: AC7 #531 («полная матрица golden зелёная: осевшие кадры не
изменились») выполнен в части, за которую отвечает эта задача — диф
#531 не добавил ни одного нового расхождения к уже принятому долгу
#530 (на r1, до ребейза, было 169/169; после ребейза те же 3 кадра,
что #530 сам назвал ожидаемыми). Не блокирую, отдельный issue не
завожу — долг уже поименован и отслеживается в #530, заводить второй
трекер того же факта было бы дублированием (§12 запрещает лишь
оставлять находку без issue, а не запрещает сослаться на уже
существующий трекинг).
## Таблица «чем краснеет» (защитные AC, #435)
Мутанты и файлы не менялись с r1 (сверено побайтно), таблица наследуется
из `docs/reviews/CODE-REVIEW-531-r1.md` целиком (AC1–AC9), с одним
дополнением:
| AC | чем доказан | чем краснеет | проверено в r2 |
|---|---|---|---|
| AC1–AC4, AC2а | `test/live-viewport.test.mjs` | 3 мутанта `scripts/mutation-gate.mjs` (`live-pan-rewrites-viewbox-every-frame`, `live-pan-writes-unchanged-viewbox`, `live-pan-shift-threshold-ignored`) | CI «Мутанты по диффу» 6/6 success на `HEAD`; методологию мутации перепроверил на новом тесте (ниже) |
| изометрия (новый, вне AC-нумерации, закрывает наблюдение r1) | тест «#531 изометрия…» | подмена `sceneFloor` на `sceneCamera` (проекция пола = проекция камеры) | **выполнением**: применил патч к `test-build/live-viewport.js`, `node --test test/live-viewport.test.mjs` → 8/9, падает именно изометрический тест; откатил, снова 9/9 |
| AC5, AC6 | `demo/smoke_live_pan_viewbox.mjs` | тот же мутант `live-pan-rewrites-viewbox-every-frame` (guard самого смока) | смок прогнан на `HEAD`: числа идентичны r1 |
| AC7 | `npm run golden:verify` | любое пиксельное отличие | прогнан: 166/169, 3 отличия — все объяснены долгом #530, не #531 (см. находку) |
| AC9 | оба changelog + `docs/ARCHITECTURE.md` | не защитный AC | чтением диффа `origin/dev...HEAD` |
## Что проверено и корректно
- Механизм не изменился с r1 ни на строку — см. «Почему разбор полный».
- Ребейз чист по коду #531: конфликт целиком в производных файлах,
разрешён пересборкой, продуктовый файл `src/live-viewport.ts` в
конфликте не участвовал (проверено по содержимому коммитов ребейза
`58e31668`/`9d1ba110`/`77a3d260` — только тесты, docs, бандл).
- Medium r1 закрыт дважды корректно: сначала `--identical` до ребейза,
затем повторно после — оба раза без изменения хотя бы одного PNG,
только отпечаток исходников.
- Новый изометрический тест — не косметика: реально ловит подмену
проекции пола на проекцию камеры (проверено мутацией).
- Провенанс: 4 коммита, каждый с `Issue: #531`, ровно один продуктовый
с `User-Visible: yes` и обоими changelog в нём же; остальные три —
документация/тест/пересборка с `User-Visible: no`, что верно (не
меняют наблюдаемое поведение).
- Бюджет бандла и типизация без регрессий.
- «Одно число — один источник»: диф не вводит ни одной новой
пользовательской величины (зоом-бейдж и его текст существовали
раньше, здесь только добавлена дедупликация записи) — неприменимо.
## Чего не проверял и почему
- **`performance_smoke` (`large-house-interaction-v1`).** Сам ТЗ выводит
миллисекунды за скобки AC; единственный судья — профиль с машины
владельца на реальном Firefox. Validate на этом SHA его тоже не гонял
(`heavy`-гейт, обычный push). Причинный механизм доказан AC1–AC6
напрямую и выполнением.
- **`node scripts/model-invariants.mjs`, `pytest tests_backend`.** Диф
не трогает геометрию/толщины/`layout`/`marker.space` и не трогает
Python — неприменимо.
- **Полная матрица смоков (243 файла).** `smoke-select.mjs` не нашёл
прямых и зарегистрированных связей, «широких» совпадений — 0.
Прогон всей матрицы для точечной правки одного модуля камеры избыточен.
- **Прямой diff против SHA r1 (`57b80f08`).** Недоступен физически —
коммит осиротел ребейзом и не резолвится ни `git cat-file`, ни
`git log --all --find-object` на дерево `2b20dd807a08`. Заменено
построчной сверкой текущего кода с описанием r1 (см. «Почему разбор
полный») — расхождений не найдено.
## Материал раунда
- Ветка: `issue/531-live-pan-transform`, коммит `77a3d2607ceed859d1244e4943c422306f34ec3e`
- Дерево материала: `c5c45966e48b9737df030a2c69e6bffe2d9bfb40`
- Вершина `origin/dev` на момент ревью: `9d35ab35ac9d02e3e0328139a73a637b356ffb63` (branch содержит весь `dev`, `git merge-base origin/dev HEAD == origin/dev`)
- Диапазон: `origin/dev...HEAD`, 9 файлов вне класса D (список — раздел «Скоуп»)
- SHA предыдущего раунда (r1): `57b80f08e63040c90ef67ff6ab7b65e4addfdc36` — мёртв после ребейза (обычный случай, §2.10, не находка)
- Вердикт этого раунда: `green` · High 0 · Medium 0 (в скоупе)
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/531-live-pan-transform`, коммит `77a3d2607cee` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `c5c45966e48b9737df030a2c69e6bffe2d9bfb40`
```
git log --all --format='%H %T' | grep c5c45966e48b
```
- Тело issue: `89e75138a50434ac52f42cef606d8af741a8ae3b022dba382c4b62c1f4887e8a`
- Вердикт конвейера: `green` · High 0