Files
houseplan-card/docs/reviews/CODE-REVIEW-122-r1.md
2026-08-13 22:04:00 +00:00

19 KiB
Raw Permalink Blame History

Код-ревью #122 — Isometric Stage 2: hidden visual polish (r1)

  • Issue: https://github.com/Matysh/houseplan-card/issues/122
  • Спецификация: docs/specs/122-isometric-stage2.md (SPEC-REVIEW-122-r1: green)
  • Диапазон: git diff origin/dev...HEAD на коммите 42b3f44 (feat: add hidden isometric stage 2), плюс сопутствующие 76ce755 (ТЗ), 4c73e2c (ревью ТЗ)
  • Роль: ревьюер кода (свежая сессия, без контекста реализации)
  • Вердикт: красный · цикл r1/4 · High: 2 · Medium: 1 → #134

Скоуп

Класс файлов коммита 42b3f44: A (src/houseplan-card.ts, src/iso-openings.ts (новый), src/iso-projection.ts, src/iso-walls.ts, src/wall-thickness.ts, src/styles.ts) + B (test/**, demo/smoke_isometric_contract.mjs, demo/smoke_isometric_live_touch.mjs, demo/golden/matrix.mjs, tsconfig.test.json) + C (docs/ISOMETRIC.md, docs/ARCHITECTURE.md, docs/STATUS.md, docs/adr/122-isometric-stage2-composition.md). Класс D (dist/**, custom_components/houseplan/frontend/houseplan-card.js, demo/srv/assets/houseplan-card.js, demo/golden/baselines/**) не изменён вовсе — см. High-1.

Трейлеры на всех трёх коммитах диапазона корректны: Issue: #122, User-Visible: no (фича скрытая, публичного changelog не требует — верно).

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

Гейт Команда Результат
Typecheck npx tsc --noEmit green, без вывода
Unit npm test green, 766/766
Build npm run build green, dist/houseplan-card.js собран
Синхронизация 3 копий бандла cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js и то же для demo/srv/assets/houseplan-card.js FAIL — расходятся, byte 45, git diff --stat 327 insertions / 209 deletions на файл; см. High-1
Целевые браузерные смоки (названы в AC1/AC5/AC6/AC9/AC10/AC11 и напрямую тронуты диффом) node demo/smoke_isometric_contract.mjs, node demo/smoke_isometric_live_touch.mjs — после npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js, т.к. коммит не обновил эту копию оба green, все проверки в JSON-выводе true
npm run golden:verify (диф меняет рендер/геометрию/слои — попадает в критерий «по необходимости») npm run golden:verify (после того же build+copy) 46/53 сценариев passed; 7 different: 5 ожидаемо-разных iso-сценариев без принятых эталонов (не блокер сам по себе, эталоны Stage 2 умышленно не принимаются на этом этапе — §12.3 ТЗ), но 2 сценария обязаны были остаться пиксель-идентичными и не остались — см. High-2, Medium-1
Дифференциальная проверка причинности (A/B) те же две команды (npm run build, npm run golden:verify) в git worktree add /tmp/dev-check origin/dev isometric-no-borders-dark и large-house-zoom-250-dark оба passed на origin/dev — регрессия строго локализована в этом диффе, не окружение
Backend не прогонялся правок в custom_components/**/*.py нет — не применимо
Performance (large-house-isometric-v1) не прогонялся по ТЗ §10/§12.4 и решению владельца это pre-beta гейт, заблокированный отдельно #124; в этом ревью не требуется

Чего не проверял и почему: полный набор из 127 браузерных смоков (диф не задевает весь продукт — только iso-поверхность и косвенно wall-thickness.ts); python -m pytest tests_backend (без py-правок); performance-профиль (вне гейта код-ревью, зависит от #124); принятие golden-эталонов (не роль ревьюера и не требуется до pre-beta).

Находки

High-1 — три копии бандла не пересобраны; CI уже красный на этом SHA

Коммит 42b3f44 не трогает dist/houseplan-card.js, custom_components/houseplan/frontend/houseplan-card.js ни demo/srv/assets/houseplan-card.js — все три остаются побитово равны промоушен-коммиту a282f85 (build: promote v1.63.0), то есть Stage-1-коду без единой строки Stage 2.

Воспроизведение:

$ npm run build
$ cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js
dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js differ: byte 45, line 1
$ git diff --stat dist/houseplan-card.js
 dist/houseplan-card.js | 536 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 327 insertions(+), 209 deletions(-)
$ grep -c "invalid isometric opening input" dist/houseplan-card.js   # fresh build
1
$ git show HEAD:custom_components/houseplan/frontend/houseplan-card.js | grep -c "invalid isometric opening input"
0
$ git show HEAD:demo/srv/assets/houseplan-card.js | grep -c "invalid isometric opening input"
0

Независимое подтверждение — CI Validate уже прогонялся на этом самом SHA и уже красный ровно на этом шаге:

$ gh run view 31746904479 --repo Matysh/houseplan-card
X frontend in 36s
  ✓ Build
  X Card bundle snapshots in sync

Хендофф-комментарий исполнителя утверждает «три отслеживаемые bundle-копии byte-identical» — это не так; либо команда не выполнялась, либо результат интерпретирован неверно. Смысл дефекта не формальный: реальный custom_components/houseplan/frontend/houseplan-card.js (то, что грузит HA) и demo/srv/assets/houseplan-card.js (то, что видит демо-стенд и что реально тестируют браузерные смоки/golden в CI до пересборки) сейчас не содержат Stage 2 вовсе. smoke/golden/performance_smoke в CI зависят от frontend (needs: frontend) и поэтому на этом SHA не запускались вообще — ни один из пре-релизных гейтов не имеет зелёного прогона против настоящего кода задачи.

Блокирует. AC16 не выполнен.

High-2 — show_borders:false в Iso не сохраняет пиксель-идентичный Stage-1 «no-volume» сценарий

ТЗ §6.6 и ADR «Consequences» требуют: при show_borders:false в Iso Stage-2-геометрия отсутствует целиком, и AC7/AC15 требуют, чтобы isometric-no-borders-dark остался неизменным. Golden показывает значительную регрессию:

$ npm run build && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
$ npm run golden:verify
different  isometric-no-borders-dark

Из artifacts/golden/golden-report.json: differingPixels: 169919, diffRatio: 0.2194 (≈22%), maxObservedDelta: 206. Повторный прогон — идентичные числа (детерминированно, не флейк). На origin/dev (та же команда, тот же git worktree add /tmp/dev-check origin/dev, тот же Chromium 151.0.7922.34) сценарий passed чисто.

Причина по коду (src/houseplan-card.ts:4503-4522, _isoScene()): рамка проекции теперь всегда строится с floorDepth: ISO_FLOOR_EDGE_HEIGHT и объединяется с isoOpeningBounds() через unionRect(flat, openingFrame) независимо от disp.showBorders / resolveIsoDecoration().structural. В Stage 1 рамка включала только wallHeight; Stage 2 добавляет глубину пола и границы проёмов всегда, даже когда сама геометрия стен/проёмов/края пола не рисуется (structural=false). Это меняет коэффициент "сжатия" floorView, и весь план визуально сдвигается/ масштабируется иначе, чем в принятом Stage-1 baseline — то есть нарушение не косметическое, это другой кадр.

Похоже на конфликт двух требований ТЗ: AC12 хочет единую рамку, не зависящую от переключения show_borders (чтобы zoom не менялся), а AC7/AC15 хотят байт-в-байт то же изображение, что и Stage 1, где такой рамки не было. Решить это — работа автора (например, включать floorDepth/openingFrame в рамку только когда structural===true, и объяснить в ADR, почему AC12 всё равно держится). Ревьюер такие продуктовые компромиссы не решает.

Блокирует. AC7 и AC15 не выполнены буквально.

Medium-1 — регрессия пикселей во Flat-сценарии при выключенном Labs → #134

large-house-zoom-250-dark (без labs/projection в demo/golden/matrix.mjs — чистый Flat, Labs неактивен) на origin/dev проходит чисто (passed), на этой ветке — нет:

different  large-house-zoom-250-dark

differingPixels: 920, diffRatio: 0.00105, maxObservedDelta: 201 — воспроизведено дважды подряд с идентичными числами (не флейк, не дрейф Chromium: версия в обоих прогонах 151.0.7922.34).

Это прямое нарушение AC2 («with Labs inactive... Flat... retain their existing state/action/pixel contract») и инварианта из docs/ISOMETRIC.md («existing Flat DOM and pixels remain the reference»). В рамках бюджета этого ревью не удалось локализовать точный узел DOM/CSS, отвечающий за эту конкретную (небольшую по площади, но не нулевую и стабильную) разницу; кандидат — новая безусловная обёртка <g class=${iso ? 'iso-floor-scene' : nothing} transform=${iso ? isoFloorMatrixCss() : nothing}> вокруг содержимого плана в src/houseplan-card.ts (добавляется в разметку независимо от того, активен ли iso), но это не доказано построчно — отметка «предположение, требует дальнейшего разбора автором», а не факт.

Площадь и видимое воздействие малы (0.1% пикселей одного сценария на большом зуме), поэтому находка не расширяет цикл #122 сверх уже блокирующих High-1/ High-2, но контракт заявлен как безусловный и находка воспроизводима — заведён отдельный issue #134 со ссылкой на #122, а не оставлена как TODO в этом документе.

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

  • AC1 (unit+smoke+код): iso остаётся единственным Labs id; demo/smoke_isometric_contract.mjs подтверждает Flat как дефолт, isometric-contract.test.mjs подтверждает отсутствие второго флага/ключа — прочитано и прогнано, зелёно.
  • AC5/AC6 (открывающиеся элементы): src/iso-openings.ts — buildIsoOpeningBasis/projectIsoOpening алгебраически повторяют существующий floor-symbol (поворот на 0–10° для gate, симметричные leaves для window, один leaf с шарниром для door); test/iso-openings.test.mjs проверяет неподвижность базиса при live-изменениях, flip-инверсию базиса, совпадение unavailable≡no-contact через openingAmount(), границы через isoOpeningBounds. Панели помечены aria-hidden="true" pointer-events="none" (src/houseplan-card.ts, рендер _renderIsoWalls) — проверено чтением и подтверждено смоком live_touch (touchOpeningIsSafe, flatIsoActionParity). Тесты умеют падать: assert.deepEqual(gateBasis.leaves.map(l => Math.abs(l.turnDeg)), [10, 10]) красится при любой правке угла; аналогично для высот leaf.
  • AC4 (floor edge): buildIsoFloorGeometry/floorFootprintGeometry — внутренние/общие границы и вложенные дырки не создают ступень (тест «canonical adjacent room union has no edge on its shared boundary», «floor edge follows outer components without internal or nested steps»), независимые partition/column не расширяют footprint (тест «Stage floor footprint excludes detached independent physical bodies»), стабильность к порядку/winding подтверждена отдельным тестом. Прочитано и прогнано зелёно; тесты специфичны настолько, что удаление одной внешней грани красит их.
  • AC8/AC14 (отсутствие второго свето-слоя, отсутствие новых сетевых/ конфигурационных путей): isometric-contract.test.mjs («Stage 2 adds no schema, dependency, storage, network or HA action surface») грепает исходники на localStorage|fetch|XMLHttpRequest| WebSocket|callService|config|schema и three|babylon|webgl — прогнано, зелёно. src/styles.ts не содержит нового источника света, только матовые градиенты/тени — прочитано.
  • AC9 (структурный кэш не растёт от HA/темы/hover): фингерпринт в _isoSource() включает rooms, walls, openCuts, openings (только геометрия/флипы), partitions, roomDrafts, columns, cellCm, gridPitch, wallKeyPitch, camera, wallHeight, floorEdgeHeight, algorithm: 3 и не включает HA-состояние/тему/hover — подтверждено и тестом isometric-contract.test.mjs («structural cache includes opening flips and excludes live HA amount», прямая проверка исходного текста _isoSource на отсутствие _openingAmt|openingAmount|.hass|matchMedia|CSS.supports| theme|hover) и смоком live_touch (sameWallFingerprint, haUpdateReusesGeometry, contactUpdateMovesOnlyLivePanel).
  • AC11 (деградация): resolveIsoDecoration — чистая функция, тест «decoration degradation never removes structure or creates floating panels» покрывает все 4 комбинации filtersSupported/forcedColors/ hideOpenings/showBorders; src/styles.ts содержит @media (forced-colors: active) и @supports not (filter: blur(1px)), оба убирают только тени/нюанс материала, не геометрию. Прочитано, логика соответствует ТЗ.
  • AC13 — намеренно не проверялось: и по ТЗ (§10/§12.4), и по решению владельца это остаётся pre-beta гейтом, заблокированным #124; хендофф честно пишет «не запускались».
  • Трейлеры коммитов, User-Visible: no, отсутствие изменений в custom_components/**/*.py, i18n и schema — проверено чтением диффа, корректно.

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

  • Полные 127 браузерных смоков — диф касается только iso-поверхности и wall-thickness.ts; прогнаны только два целевых, названных в AC и прямо изменённых в этом диффе.
  • python -m pytest tests_backend — нет изменений в Python.
  • large-house-isometric-v1 performance — вне гейта код-ревью при открытом #124 (см. ТЗ §10, решение владельца).
  • Принятие golden-эталонов — не роль ревьюера; данные из artifacts/golden/golden-report.json использованы только как диагностика причинности регрессии, не как повод принять/отклонить baseline.
  • Точная DOM/CSS-причина Medium-1 не изолирована построчно — честно оставлено как открытый вопрос автору в #134.

Итог

Две High-находки блокируют цикл r1: код не собран в те артефакты, которые реально исполняются (High-1, CI уже подтверждает это независимо), и Stage 2 в текущем виде ломает пиксельный контракт, который ТЗ объявляет безусловным — show_borders:false в Iso (High-2). Третья находка (регрессия чистого Flat) воспроизводима, но мала по воздействию — заведена как Medium #134, а не оставлена как TODO. Работа предметно сильная — покрытие юнит-тестами геометрии открытий и floor edge выдерживает мутации, изоляция структурного/live кэша сделана аккуратно и подтверждена и тестами, и смоками — но AC16, AC7 и AC15 не могут считаться выполненными до пересборки бандла и разбора причины пиксельной регрессии show_borders:false.