Files
houseplan-card/docs/reviews/CODE-REVIEW-651-r1.md
T
2026-09-26 01:30:54 +00:00

265 lines
25 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-651-r1
Материал раунда: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`,
SHA `c40266431ca1335ed09ebbaace9656b55ee84e23` (рабочая копия уже на нём).
Коммиты: `4ab7ecb4` (`fix(2.5D): keep device overlays stable while zooming`,
Issue #651, User-Visible: yes) и `c4026643` (`test(2.5D): retire superseded
zoom mutant`, Issue #651, User-Visible: no).
## Скоуп
Задача переводит вычисление положения HTML-overlays устройств и lock badges в
2.5D в стабильный масштаб «вписать всё» (`referenceView`) и заменяет
независимое попарное разведение значков (`resolveIsoOverlayCollisions`) на
жёсткую групповую раскладку (`resolveIsoOverlayRigidGroups`): все значки одной
комнаты, чьи расширенные на 12 CSS px базовые bounds пересекаются, получают
один общий вектор сдвига. Затронуты `src/iso-overlays.ts` (+338 строк, новый
резолвер), `src/iso-scene-render.ts` (кэш/подпись раскладки берёт
`referenceView`, а не live `view`), `src/houseplan-card.ts` (передаёт
структурный `_baseVb('iso', structural)` как `referenceView`), тесты,
`docs/ISOMETRIC.md`, оба changelog, `docs/STATUS.md`, `scripts/mutation-registry.mjs`.
Список затронутых модулей совпадает с §14 ТЗ, выхода за список нет.
Соответствие `docs/SCOPE.md`: правка чинит J1/J6 (спатиальный обзор дома
остаётся читаемым при зуме, план остаётся «true» после действий пользователя)
в рамках уже принятого J1-исключения #89 на публичный 2.5D-режим; новых
поверхностей, настроек или охвата не добавляет.
## Как проверялось
Валидация на `c4026643` уже зелёная
(https://github.com/Matysh/houseplan-card/actions/runs/36206722475) —
дешёвые гейты (`tsc`, `npm test`, `build`, мутанты по диффу) не перегонялись
повторно вручную сверх точечной проверки ниже; тяжёлые прогнаны мной лично на
этом же SHA, т.к. Validate явно пропустил их (`skipped`, reuse-маркер) для
обоих коммитов диапазона.
| Гейт | Статус | Кем/чем подтверждено |
|---|---|---|
| `npx tsc --noEmit` | 🟢 | часть `npm run bundle:sync`, прогнан лично; 0 ошибок |
| `npm test` (3099 тестов) | 🟢 (не дублировал) | job «Фронтенд: типы, юниты, мутанты, синхрон бандла» зелёной Validate на этом SHA; лично прогнал целиком только `test/iso-overlays.test.mjs` + `test/iso-scene-render.test.mjs` (50/50) при верификации мутаций ниже |
| `npm run build` + сверка 3 копий бандла | 🟢 | прогнан лично (`bundle:sync`); `git status` после сборки — 0 диффов в `dist/`, `custom_components/.../frontend/`, `demo/srv/assets/` |
| `node scripts/check-docs.mjs` | 🟢 (warn, как в CI) | прогнан лично с `--screenshots=$(node scripts/classify-changes.mjs --screenshots-mode)` → `warn`; единственное сообщение — ожидаемый WARN «screenshot source fingerprint is stale… before the beta candidate (#479)», это системное следствие любой правки `src/**` и не блокирует ни в CI, ни здесь (в `strict`-режиme без флага та же строка попадает в errors — не имеет отношения к #651, это дефолт скрипта, а не регресс) |
| `demo/smoke_isometric_contract.mjs` | 🟢 | прогнан лично: все 46 проверок true, включая новую `desktopZoomKeepsOverlayScene` |
| `demo/smoke_isometric_live_touch.mjs` | 🟢 | прогнан лично: все проверки true, включая новую `touchPinchKeepsOverlayScene` |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 🟢, изучен | 8 прямых совпадений — все на `_openingsR` (проброшен без изменений в новый вызов, не новая логика); 35 слабых связей — все на `_baseVb` (реализация функции не менялась, только новый вызывающий код с явными аргументами); ни один из смоков сверх двух уже прогнанных изометрических не выбран для запуска — риск ограничен `iso-overlays.ts`/`iso-scene-render.ts`/4 строк в `houseplan-card.ts`, что уже покрыто прогнанными изометрическими смоками |
| `npm run golden:verify` (полная матрица, видимое изменение) | 🟡 см. ниже | прогнан лично целиком (245 сценариев) на этом SHA после `npm run bundle:sync` |
| `npm run benchmark:large-house-isometric` | 🟢 | прогнан лично на этом SHA: `sourceSha=c4026643…`, `panZoomMs` median 67.3 / p95 133.7 мс — бюджет `budgets-isometric-smoke.json` (`panZoomMs.hardMaxMs=600`, absolute) кратно не задет; сам absolute-гейт уже зелёный в Validate |
| `npm run benchmark:isometric-stage3-dense` | 🟢 | прогнан лично на этом SHA: `panZoomMs` median 73.4 / p95 75.8 мс, тот же запас |
| `npm run invariants -- --config …` | не прогонял | диффа геометрии модели/конфигурации нет — сдвиг overlay - runtime-only presentation state, не сериализуется (§7 ТЗ, подтверждено чтением: `nudgeScene`/`visualScene` нигде не пишутся в `RoomCfg`/`marker`) |
| `python -m pytest tests_backend` | не прогонял | диффа `custom_components/**/*.py` нет |
| Мутационные свидетели #651 (3 новых) | 🟢 | зафиксированы «Мутанты по диффу (1–6/6)» зелёной Validate на `c4026643`; дополнительно перечитаны сами патчи (см. находки о непокрытых веток ниже) |
### Golden: что показал полный прогон
5 из 245 сценариев вышли `different` — все прямо относятся к 2.5D-оверлеям
(остальные 240, включая `isometric-geometry-view-*`, `isometric-touch-kiosk-dark`,
`isometric-wall-junctions-dark`, Flat-эквивалент `large-house-warm-remount-dark`
на той же `perf-floor-2` фикстуре — не изменились):
- `isometric-stage3-overlays-light` (diffRatio 1.76 %), `-dark` (2.10 %),
`isometric-stage3-forced-colors-dark` (0.94 %), `isometric-stage3-no-filter-dark`
(0.97 %) — визуально проверены: значок/бейдж и связанный с ним lock badge той
же комнаты сдвигаются вместе на несколько CSS px в сторону от стены; ряд и
взаимные интервалы сохраняются, диагонального распада нет.
- `isometric-large-warm-remount-dark` (8.21 %) — самый крупный диффа. Проверено
по кропам (`Room 2.1` и соседние): **старый** эталон рисовал в этой
стресс-фикстуре по 2–3 визуально разведённых значка на комнату, где во Flat
(`large-house-warm-remount-dark`, тот же `perf-floor-2`, не изменился и
остался `passed`) на то же место приходится **ровно один** значок —
т.е. старый попарный резолвер `resolveIsoOverlayCollisions` расталкивал
канонически совпадающие точки, которых в Flat не существует. Новый код
показывает ровно один значок на комнату, как во Flat. Это не регрессия, а
прямое следствие требования §5.2.3 ТЗ («горизонтальный ряд… остаётся…»,
обобщённо — совпадающие точки остаются совпадающими) и корректный результат.
Новые baseline-PNG в диффе НЕ приняты — это правильно и соответствует AC7
(«новый baseline принимается только из reviewed Linux artifact в ближайшем
beta-candidate, а не самовольно в author-коммите»). Формулировка автора в
комментарии к реализации («визуальный контракт проверяется существующими
Linux golden-сценами isometric-stage3-*») неточна: существующие эталоны сейчас
**не совпадают** с рендером этого SHA — обязательство §10.4 ТЗ («golden — перед
beta по процессу») выполнено правильно тем, что PR не подделал их
самостоятельно, но фраза «проверяется существующими сценами» вводит в
заблуждение (эталоны как раз показывают ожидаемое расхождение, а не
подтверждение). На вердикт это не влияет — сути AC7 (проверка ревьюером
«одного общего сдвига без диагонального распада») сделанный мной прогон и
разбор изображений закрывают.
## Находки
### Medium-1 (в скоупе, возврат автору) — AC1 «другая комната исключена» не доказан ни тестом, ни мутацией
`resolveIsoOverlayRigidGroups`/`rigidOverlayGroups` (`src/iso-overlays.ts:1738`)
объединяет в одну жёсткую группу значки только при совпадении
`placement.owner?.id`. Это ключевая часть AC1 («device markers и lock badges
одной комнаты образуют ожидаемые компоненты; другая комната… исключены») и
явно обещанного теста в §10.1 ТЗ («матрица групп… перестановок»). Единственный
новый тест (`test/iso-overlays.test.mjs`, «rigid overlay groups preserve a
row…») проверяет только один сценарий — три значка ОДНОЙ комнаты. Ни один тест
не ставит рядом значки двух РАЗНЫХ комнат.
**Воспроизведение (чем краснеет — точнее, чем не краснеет).** Закомментировал
проверку владельца в join-цикле:
```ts
// было: if (stable[right].placement.owner?.id !== owner) continue;
// стало: // mutated: room ownership check removed
```
Пересобрал `test-build` (`npx tsc -p tsconfig.test.json && node
scripts/fix-test-build.mjs`) и прогнал `node --test test/iso-overlays.test.mjs
test/iso-scene-render.test.mjs` — **50/50 тестов прошли**, ни один не заметил,
что значки двух разных комнат теперь могут объединяться в одну жёсткую группу
и сдвигаться единым вектором через границу комнаты. Правка отменена
(`git diff` пуст, репозиторий не тронут).
Риск: будущий рефакторинг рядом (например, изменение условия соединения) может
незаметно позволить группе «перетекать» через границу комнаты — именно то,
для защиты от чего ТЗ явно требует этот тест.
### Medium-2 (в скоупе, возврат автору) — AC4 «деградированный fallback группы» не доказан ни тестом, ни мутацией
§5.3.5 ТЗ — самая рискованная новая ветка (собственный риск №1/2 из §11 ТЗ):
если ни один кандидат-сдвиг не свободен от конфликтов, `resolveIsoOverlayRigidGroups`
выбирает «ближайший вектор, который сохраняет принадлежность комнате и
минимизирует сначала конфликт со стеной, затем с другой группой» — реализовано
в `rigidFallbackOrder` (`src/iso-overlays.ts:1690`): сравнение по
`roomViolations`, затем `wallViolations`, затем `overlapPenalty`. §10.1 ТЗ прямо
обещает тест на «невозможного размещения и границы 48 px» в
`test/iso-overlays.test.mjs`. Такого теста нет; нет и мутанта в
`scripts/mutation-registry.mjs`, нацеленного на эту функцию (три новых мутанта
бьют по: масштабу раскладки, факту соединения в группу, факту использования
нового резолвера в проде — не по порядку приоритетов деградации).
**Воспроизведение.** Заменил тело `rigidFallbackOrder` на «игнорировать
room/wall/overlap, сравнивать только по расстоянию»:
```ts
function rigidFallbackOrder(left: RigidCandidate, right: RigidCandidate): number {
return rigidOffsetOrder(left.offset, right.offset);
}
```
Пересобрал test-build и прогнал те же два файла — **50/50 тестов прошли**.
Ни один существующий тест не ставит группу в положение, где после перебора всех
кандидатов остаётся конфликт, поэтому приоритет «сначала не покинуть комнату,
потом не задеть стену, потом не пересечься с другой группой» сейчас ничем не
защищён: обратный порядок или полное игнорирование прошли бы CI незамеченными.
Правка отменена, `git diff` пуст.
Обе находки — Medium, в скоупе задачи (тесты этого самого файла, обещанные в
уже одобренном ТЗ), без High. По правилу §3 п.8/§2.7 это жёлтый вердикт и
возврат автору в этом же issue, а не отдельная задача.
### Low — неточная формулировка в комментарии к реализации (снято без правки)
Комментарий автора к реализации утверждает, что визуальный контракт
«проверяется существующими Linux golden-сценами» — на деле эти сцены сейчас
показывают ожидаемое расхождение (см. раздел Golden выше), а не подтверждение.
Не меняет вердикт и не требует действий в коде/ТЗ — фиксирую только для
точности учёта golden-цикла на бете; правку не считаю нужной.
## Что проверено и признано корректным
- **AC3 (инвариантность zoom/pan).** `layoutView = input.referenceView ||
input.view` (`src/iso-scene-render.ts:806`); `_baseVb('iso', structural)`
берёт `scene.frame`/`_frameOf().rect`, т.е. геометрию фита, а не live
`_view` (`src/houseplan-card.ts:6054`, `6075-6092`) — подтверждено чтением и
тестом `#651 supersedes #473 W3` (`test/iso-scene-render.test.mjs`), который
меняет `view` без изменения `referenceView` и требует `strictEqual` того же
объекта размещения; мутант `iso-rigid-groups-use-live-zoom-scale` ловится этим
же тестом (подтверждено зелёной Validate). Браузерные смоки
`desktopZoomKeepsOverlayScene`/`touchPinchKeepsOverlayScene` подтверждают то
же в реальном DOM (прогнано лично).
- **AC2 (один вектор на группу, ряд остаётся горизонтальным).** Тест «rigid
overlay groups preserve a row…» и «render scene keeps a close device cluster
rigid…» проверяют `nudgeCss`/`visualScene` идентичность внутри группы и
инвариантность к порядку входа; мутант `iso-rigid-groups-split-close-row`
(отключение join) ловится первым тестом (зелёная Validate).
- **Производственная проводка (AC — переключение на новый резолвер).**
`src/iso-scene-render.ts:987` вызывает `resolveIsoOverlayRigidGroups` только
в `mode === 'live'`; `resolveIsoOverlayCollisions` в проде более не
вызывается (`grep` подтверждает единственное вхождение — сама декларация).
Мутант `iso-scene-restores-per-marker-collision-resolver` возвращает старый
вызов и ловится тестом «keeps a close device cluster rigid» (зелёная
Validate).
- **Room labels не входят в группу.** `entries.flatMap` в
`buildIsoOverlayRenderScene` явно отфильтровывает `entry.kind ===
'room-label'` перед передачей в `resolveIsoOverlayRigidGroups`
(`src/iso-scene-render.ts:988-989`) — проверено чтением, не тестом
(структурная гарантия, не защитный AC — низкий риск регрессии, т.к. типы
`IsoOverlayCollisionKind` уже исключают `'room-label'` на уровне TypeScript).
- **48 px cap и safety-gap глобально.** `addOffset` в
`resolveIsoOverlayRigidGroups` отбрасывает кандидатов с `distance > maxNudge
+ EPS`; кандидаты строятся из `buildIsoOverlayBoundaryCandidates`/
`addBoundaryCircle`, оба уже покрыты существующими unit-тестами на границу.
Не покрыт именно ВЫБОР среди легальных-по-cap, но конфликтующих кандидатов —
см. Medium-2.
- **AC8 (перформанс).** Оба профиля прогнаны мной лично на точном SHA
`c4026643`, бюджетные файлы (`demo/performance/*.json`) не менялись в диффе.
- **Трейлеры и changelog.** `Issue: #651` + `User-Visible: yes|no` на обоих
коммитах; `docs/CHANGELOG.md`/`docs/CHANGELOG.ru.md` правлены в том же
коммите `4ab7ecb4`, что и код (не в последующем). Одно число с одним
источником: константы `ISO_OVERLAY_MAX_NUDGE_CSS_PX` (48),
`ISO_OVERLAY_RIGID_GROUP_GAP_CSS_PX` (12), `ISO_OVERLAY_SAFETY_GAP_CSS_PX` (4)
объявлены один раз и переиспользованы; `docs/ISOMETRIC.md` описывает те же
числа словами, не дублирует их как отдельный источник правды.
- **Три копии бандла.** После `npm run bundle:sync` `git status` не показал
диффов ни в `dist/`, ни в `custom_components/houseplan/frontend/`, ни в
`demo/srv/assets/` — сборка детерминирована и уже соответствует материалу.
- **docs/reviews/INDEX.md** обновлён автогенератором и синхронен со SPEC-REVIEW
r1/r2 этого issue.
## Чего не проверял и почему
- **Полный `npm test`** — не перегонял целиком лично (только два целевых
файла много раз при верификации мутаций); полагаюсь на зелёный job
«Фронтенд…» в уже подтверждённой Validate на этом самом SHA (дешёвый гейт,
инструкция явно разрешает не дублировать).
- **35 «слабых» смоков из `smoke-select.mjs`** (все на `_baseVb`) — не
прогонял: сигнатура и реализация `_baseVb` не менялись, только добавлен один
новый вызывающий код; риск ограничен уже прогнанными изометрическими
смоками.
- **`npm run invariants`** — геометрия плана/стен/проёмов не меняется (это
presentation-only runtime cache для overlay), поэтому гейт не применим; не
формальный пропуск, а обоснованный чтением §7 ТЗ и кода (`nudgeScene`
нигде не попадает в `RoomCfg`/config).
- **`pytest tests_backend`** — нет диффа в `custom_components/**/*.py`.
- **Golden `main`/предрелизный accept** — не выполнял и не должен: AC7 прямо
запрещает принимать новый baseline в author-коммите; свою часть (просмотр
diff-изображений, суждение «один сдвиг, не диагональ») выполнил вручную по
выгруженным PNG этого прогона.
- **`isometric-stage3-openings-dark` и другие golden с opening/lock фокусом** —
прошли (`passed`) сами, детально не разбирал сверх этого.
## Вердикт
Оба AC, вынесенных в находки, входят в собственный (уже одобренный, зелёный на
ревью ТЗ) тестовый план задачи (§10.1) и являются защитными/инвариантными по
характеру (границы группы по комнате; порядок приоритетов деградированного
fallback) — по правилу REVIEWER.md пустая графа «чем краснеет» для такого AC
есть находка Medium сама по себе, а обе находки здесь дополнительно
подтверждены явным мутационным экспериментом (50/50 тестов проходят при
удалении защиты). High-находок нет, реализация в остальном корректна,
инвариантность к zoom/pan и групповая раскладка проверены чтением, тестом и
исполнением (unit + 2 браузерных смока + перформанс на точном SHA + ручной
разбор golden-диффов). Вердикт — жёлтый, работа возвращается автору: дополнить
`test/iso-overlays.test.mjs` (и по необходимости `mutation-registry.mjs`)
сценариями «две разные комнаты не объединяются» и «невозможное размещение
внутри 48 px», как было обещано в §10.1 уже одобренного ТЗ.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/651-iso-device-layout-stability`, коммит `c40266431ca1` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `7afb4dcc4787ceabd952ead5915616bd806a98b2`
```
git log --all --format='%H %T' | grep 7afb4dcc4787
```
- Тело issue: `699e063899bf674ad775879fc1a0c889489a2f90b14fde467497787cc8f69442`
- Вердикт конвейера: `yellow` · High 0