mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
242 lines
23 KiB
Markdown
242 lines
23 KiB
Markdown
# 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
|