25 KiB
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-цикле:
// было: 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, сравнивать только по расстоянию»:
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.
- EPS
- 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:syncgit 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 уже одобренного ТЗ.
Материал раунда
- Ветка:
issue/651-iso-device-layout-stability, коммитc40266431ca1— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
7afb4dcc4787ceabd952ead5915616bd806a98b2git log --all --format='%H %T' | grep 7afb4dcc4787 - Тело issue:
699e063899bf674ad775879fc1a0c889489a2f90b14fde467497787cc8f69442 - Вердикт конвейера:
yellow· High 0