Files
2026-09-30 08:11:47 +00:00

14 KiB
Raw Permalink Blame History

CODE-REVIEW-711-r1

Issue: #711 · «2.5D: значки не сдвигаются от состояния устройства; HA-обновление не переразмещает оверлеи» Трек: show (владелец задал поведение решением от 2026-09-30) Материал: a372b354c6f0e47902dbf81ed383e4e3ca637034, один коммит поверх dev@9caa3c23 Заход: r1 · блокирующих циклов израсходовано 0 из 2

Скоуп

Часть #694 (регрессия производительности 2.5D после #651/beta.4). Три AC:

  • AC1 — раскладка устройства видит state-free плитку (без текста значения, бейджей, доп. метрик); смена состояния не двигает значки и не запускает поиск столкновений; видимая ширина (с бейджем) по-прежнему учитывается в границах сцены.
  • AC2 — поиск жёстких групп отсекает кандидатов по лексикографической границе (комната → стены → перекрытие) без изменения результата.
  • AC3 — stateUpdate на isometric-stage3-dense-v1 не выше, чем у v1.77.0.

Отношение к docs/SCOPE.md: правит J1 («live spatial overview») и детерминированность узкого исключения 2.5D (#89 — «то же состояние/действия/ геометрия, что и в плоском виде»); дрейф позиции значка от состояния нарушал именно этот принцип. В скоупе.

Как проверялось

Прочитаны: docs/SCOPE.md, AGENTS.md, docs/process/REVIEWER.md, docs/ISOMETRIC.md (канон подсистемы), тело issue #711 и оба комментария («Взял», «Сделано»). Дифф разобран построчно (git diff origin/dev...HEAD) по обоим правленым файлам src/iso-scene-render.ts и src/iso-overlays.ts.

Отдельно проверил цепочку типов и вызовов:

  • откуда берётся layoutHalfSize для устройства (stateFreePresentation → isoRaisedOverlayHalfSize) и что isoRaisedOverlayHalfSize для kind: 'device' зависит только от valueText, valueFullText, valueBadge, legacySupplementalMetrics (которая читает tempText/humText) — stateFreePresentation обнуляет ровно эти пять полей (src/iso-scene- render.ts:150-155, src/iso-scene-render.ts:290-310, src/device-face.ts:35-48). Пропущенных состояние-зависимых полей, влияющих на размер, не нашёл.
  • что layoutHalfSizeOf(entry), а не entry.screenHalfSize, идёт в place() (раскладка/коллизии) и в collisionSignature, тогда как screenHalfSize (визуальный, состояние-зависимый) остаётся источником для границ сцены (overlayEntryPoints, src/iso-scene-render.ts:651-668) — то есть ровно то разделение, которое требует AC1.
  • быстрый путь «HA-only change» (src/iso-scene-render.ts:1001-1023): срабатывает только когда collisionSignature (по layoutHalfSizeOf, местам и владению комнатой) не изменился; обновляет только screenHalfSize у затронутых записей и возвращает тот же Map devices/ rooms/locks без обращения к resolveIsoOverlayRigidGroups — то есть действительно «поиск столкновений не запускается».
  • пруннинг в resolveIsoOverlayRigidGroups (src/iso-overlays.ts:1883-1968): проверил как branch-and-bound относительно rigidFallbackOrder (roomViolations, wallViolations, overlapPenalty, offset, src/iso- overlays.ts:1690-1694). Отсечение по bound строго монотонно (кандидат хуже текущего fallback по primary/secondary измерению не может ни стать chosen, ни заменить fallback), кандидат с 0/0 нарушений никогда не отсекается — победитель не может быть потерян отсечением. Логика соответствует комментарию AC2.

Исполнение, не только чтение, для AC1: временно подменил src/iso-scene-render.ts на версию origin/dev (без переключения HEAD, без коммита), пересобрал (npx tsc -p tsconfig.test.json → node scripts/fix-test-build.mjs) и прогнал только новый тест:

node --test --test-name-pattern="#711" test/iso-scene-render.test.mjs

Результат на коде до фикса: # fail 1 — падает на assert.strictEqual(on.devices.get(id), off.devices.get(id)) для lamp (объект раскладки — новый экземпляр, полная рекомпоновка сработала). Затем вернул файл материала обратно, пересобрал и прогнал тот же тест повторно: # pass 1. Рабочая копия после эксперимента чиста (git status --short пусто, git rev-parse HEAD не менялся) — эксперимент не оставил следов в репозитории.

Проверил, что мутант iso-device-layout-follows-state-again (scripts/mutation-registry.mjs) целится ровно в изменённую строку (presentation: stateFreePresentation(presentation) → presentation) — совпадает с тем, что тест выше проверил вручную.

Для AC2 «чем краснеет» — существующий (не изменённый в этом диффе) тест test/iso-overlays.test.mjs:179 «rigid fallback prioritizes room, then wall, then overlap inside the 48px cap» уже кодирует именно порядок приоритетов, который защищает пруннинг; он входит в зелёный npm test из Validate на этом SHA.

Проверил трейлеры коммита: Issue: #711, User-Visible: yes, оба docs/CHANGELOG.md/docs/CHANGELOG.ru.md правлены в том же коммите, формулировки согласованы с docs/USER-GUIDE.ru.md («Объёмный вид плана (2.5D)», docs/USER-GUIDE.ru.md:60,345).

Числа (§8, «один источник»): единственные пользовательские/оценочные числа — таблица бенчмарка (stateUpdate 208/522/89 мс и т.д.) — присутствуют только в комментарии «Сделано» и в теле коммита, и совпадают между собой; ни в docs/CHANGELOG*.md, ни в docs/ISOMETRIC.md эти числа не повторяются — дублирования с риском расхождения нет.

Что проверено и корректно

  • AC1 — доказан исполнением (тест красный на dev-версии файла, зелёный на материале) плюс чтением, что раскладка/коллизии видят state-free размер, а видимая ширина с бейджем — по-прежнему в границах сцены (тот же тест проверяет isoOverlaySceneBounds(on).w >= isoOverlaySceneBounds(off).w). Быстрый путь при неизменной раскладке действительно не вызывает resolveIsoOverlayRigidGroups.
  • AC2 — доказан чтением (пруннинг математически корректен относительно объявленного лексикографического порядка) плюс исполнением существующего теста на приоритет room→wall→overlap (часть зелёного npm test). Автоматической регрессии именно на «хэш плотной сцены не меняется» нет — это ручной разовый замер автора в комментарии (−2128229368), не закреплённый тестом; для AC с формулировкой «Проверка: существующие тесты… и хэш одинаков» это ожидаемый уровень доказательства (AC не требовал нового теста на хэш), не нахожу это находкой.
  • AC3 — задокументирован таблицей замера в комментарии «Сделано»: одна песочница, 3 сэмпла, stateUpdate 89 мс против 208 мс у v1.77.0 (ниже порога, AC выполнен с запасом). Способ доказательства AC3 сам по себе — «замер в «Сделано»», не автотест; замер приведён с названным профилем (isometric-stage3-dense-v1) и числами по трём итерациям, соответствует формату §8.
  • Трейлеры коммита, оба changelog, терминология из USER-GUIDE — в порядке.
  • Мутант зарегистрирован и целится точно в защищаемую AC1 строку.
  • Изменение не трогает модель геометрии (src/plan-geometry-preflight и т. п.) — только раскладку/коллизии оверлеев в 2.5D; npm run invariants не относится к этому диффу.

Находки

Нет ни High, ни Medium, ни Low.

Чего не проверял

  • Полный npx tsc --noEmit / npm test / npm run build с сверкой трёх копий бандла — не гонял заново: Validate на этом SHA (a372b354) зелёный (https://github.com/Matysh/houseplan-card/actions/runs/36686903130), §8/AGENTS.md разрешают на него сослаться. Прогнал только точечный node --test для одного файла (см. выше) — это не замена полного набора, а точечная проверка «умеет ли тест падать».
  • Браузерные смоки не гонял. node scripts/smoke-select.mjs --base origin/dev --head HEAD вернул НЕОПРЕДЕЛЁННОСТЬ (дифф исполняемый, но ни один смок не связан доказуемо; новые символы вроде layoutHalfSizeOf, isoRaisedOverlayHalfSize, RigidCandidate не встречаются ни в одном смоке). Тело issue #711 не называет ни одного смока в AC, так что запуск Chromium не требуется по правилам этого прогона. В комментарии «Сделано» автор называет команды и зелёный результат для визуального минимума (8 смоков) и smoke_isometric_contract, smoke_isometric_live_touch — это засчитывается как доказательство (команда и результат названы), но я не переисполнял их независимо.
  • npm run golden:verify — метки ci:golden на этой задаче нет, дифф не содержит правок demo/golden/**.
  • python -m pytest tests_backend — дифф не касается custom_components/**/*.py.
  • npm run invariants — дифф не касается геометрии модели плана (комнаты/ стены/маркеры), только раскладку 2.5D-оверлеев; не применимо.
  • Мутации по диффу (в т. ч. iso-device-layout-follows-state-again) не гонял — по правилам трека show мутанты в разработке не гоняются, их ловит ночной прогон (#709); я лишь сверил регистрацию и цель патча.
  • Продуктовое поведение с бейджами, наезжающими на соседний значок в плотном ряду (риск, названный самим автором) — не проверял визуально/скриншотом; это заявленное и принятое владельцем следствие правила «положение не зависит от состояния», не дефект этой задачи.

Вердикт

Зелёный. Оба изменённых файла реализуют ровно то, что заявлено в AC; AC1 доказан исполнением в обе стороны (красный → зелёный), AC2 — чтением плюс существующим регрессионным тестом на порядок приоритетов, AC3 — замером с названным профилем. Трейлеры, changelog и терминология в порядке. Находок нет.


Материал раунда

  • Ветка: issue/711-iso-overlay-state-free-layout, коммит a372b354c6f0 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 25a16d0f804ada19deba6c12c8a2dfa6a19f48fb
    git log --all --format='%H %T' | grep 25a16d0f804a
    
  • Тело issue: 14d76d92f923bb2922a3dc0c35ce91a8927a24a572e525d513a116db2c51d319
  • Вердикт конвейера: green · High 0