19 KiB
Код-ревью #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-v1performance — вне гейта код-ревью при открытом #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.