Files
houseplan-card/docs/reviews/CODE-REVIEW-89-r2.md
T
2026-08-13 12:53:16 +00:00

27 KiB
Raw Blame History

CODE-REVIEW-89-r2 — #89, этап 1: объёмный вид за флагом Labs

  • Issue: #89
  • Этап: code (PROCESS.md §2.7)
  • Диапазон: origin/dev...HEAD, origin/dev = 22e98c5, HEAD = 6ea3ebf (ветка issue/89-isometric-stage1, детач HEAD)
  • ТЗ: docs/specs/089-isometric-view-stage1.md, ревизия 3, ревью ТЗ зелёное — SPEC-REVIEW-89-r1.md
  • Цикл: r2/4
  • Вердикт: красный

Скоуп ревью

35 файлов, +3079/−825 (генерируемые бандлы dist/**, custom_components/houseplan/frontend/**, demo/srv/assets/houseplan-card.js — класс D, сверены байт-в-байт с npm run build). Продуктовый код: src/labs.ts (новый), src/iso-projection.ts (новый), src/iso-walls.ts (новый), src/houseplan-card.ts (+286/−18), src/styles.ts (+27), src/i18n/{en,ru}.json (+2 ключа каждый). Тесты/гейты (класс B): test/iso-projection.test.mjs, test/iso-walls.test.mjs, test/labs.test.mjs, test/isometric-contract.test.mjs, test/golden-matrix.test.mjs (diff), demo/smoke_isometric_contract.mjs, demo/smoke_isometric_live_touch.mjs, demo/golden/{harness,matrix}.mjs, demo/benchmark_large_house.mjs, demo/performance/{card-contract.mjs,budgets-large-house-isometric.json,README.md}, .github/workflows/performance.yml, tsconfig.test.json, package.json. Документация (класс C): docs/SCOPE.md, docs/ISOMETRIC.md (новый), docs/adr/089-isometric-stage1-renderer.md (новый), docs/DEVELOPMENT.md, docs/STATUS.md, AGENTS.md, docs/specs/README.md. Backend (custom_components/houseplan/**/*.py), manifest.json, hacs.json, README/User Guide/оба changelog — не тронуты (проверено git diff --stat), что соответствует D1/D7/AC12/AC15 ТЗ.

Прочитано до вердикта: docs/SCOPE.md, AGENTS.md, PROCESS.md, тело issue #89 и все 15 комментариев (включая PSEUDO_3D_SPECIFICATION.md и решения владельца Q1–Q6/O1–O6), docs/specs/089-isometric-view-stage1.md (ревизия 3), SPEC-REVIEW-89-r1.md, docs/ISOMETRIC.md, ADR, docs/CANVAS.md, docs/WALL-THICKNESS.md, docs/LIGHT.md, docs/UX-MODES.md, docs/TOUCH-SUPPORT.md, весь новый и изменённый продуктовый код.

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

npm ci выполнен перед гейтами (чистая рабочая копия, зависимостей не было).

Гейт Команда Результат
Типы npx tsc --noEmit green
Unit npm test 724/724 green
Сборка npm run build green
Синхронность бандлов cmp dist/… custom_components/… и cmp dist/… demo/srv/… обе пары идентичны байт-в-байт
Whitespace git diff --check origin/dev...HEAD 3 предупреждения — см. Low L1
Backend (чистый, без HA) python -m pytest tests_backend -q pytest не установлен в среде; неприменимо — диапазон не трогает custom_components/**/*.py (git diff --stat пуст), гейт пуст по построению
Браузерные смоки (все 127) for f in demo/smoke_*.mjs; do node "$f"; done (тот же цикл, что job smoke в validate.yml) 126/127 green, 1 red — см. находку H1
Golden capture (весь матрикс v17) npm run golden:capture все существующие плоские сцены — passed (0 diff); 6 новых isometric-* сцен — missing-baseline (ожидаемо: новые эталоны не принимаются в этом коммите, см. AC/§11.3 ТЗ и PROCESS.md §12)
Perf-профиль (сквозной прогон, не CI-гейт) node demo/benchmark_large_house.mjs --profile=large-house-isometric-v1 --samples=1 --warmups=1 выполняется целиком, viewToggleMs и isoGeometry cache size присутствуют в отчёте

Установка headless Chromium (npx playwright install --with-deps chromium) потребовалась и прошла успешно — без неё браузерные смоки/golden в CI-стиле были бы недоступны локально, а именно они отвечают на вопрос «оно вообще работает» для AC1–AC2, AC4, AC7–AC9, AC13–AC14.

Мутанты (§11.4 ТЗ) — выборочно исполнены

  • Мутант 1 («iso без флага») — покрыт demo/smoke_isometric_contract.mjs (flatDefault, removalIsImmediateFlat) — прошёл.
  • Мутант 4 («expires игнорируется») — test/labs.test.mjs 'registry metadata and numeric-core lifetime fail closed' — прошёл; проверено также вручную (liveLabsFlags('1.65.0-beta.1') → флаг мёртв).
  • Мутант 5 («боковые грани слоями») — test/iso-walls.test.mjs 'one top and only O(E) visible sides...' (sides.length <= edgeCount) — прошёл; инъекция «удвоить sides без увеличения edgeCount» вручную в Node REPL красит именно этот assert.
  • Мутант про инъекцию рендер-исключения (_isoSource throws) — demo/smoke_isometric_contract.mjs (fallbackLatched, explicitRetryRestoresIso) — исполнен реальным браузером, прошёл; убедился, что смок умеет падать: временно заменил shouldFail = false на true без сброса — explicitRetryRestoresIso покраснел, как и ожидалось.

Целевая проверка «работает ли AC7 на самом деле» (H1)

demo/smoke_isometric_live_touch.mjs красит liveLayersPresent и floorToOverlayOrderPreserved. Изолировал причину отдельным скриптом: .sunlayer не монтируется ни в flat, ни в iso при заданных в фикстуре параметрах (sun.sun.attributes.azimuth=180, окно angle:0, y:0.14 — северная стена). Прогнал computeSunRays-путь через реальную карточку при азимутах 0/45/…/315°: .sunlayer появляется только на 0°/45°/315°, никогда на 180°. Это математически корректное поведение computeSunRays (windowLit() не пропускает свет через окно, когда солнце светит с противоположной стороны дома) — то есть не регрессия рендерера, а ошибка в геометрии самой фикстуры смока. Подробности — в находке H1.

Находки

H1 — обязательный по ТЗ смок demo/smoke_isometric_live_touch.mjs красный на сданном коммите

Файл: demo/smoke_isometric_live_touch.mjs Серьёзность: High — блокирует.

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

npm run build && cp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js \
  && cp dist/houseplan-card.js demo/srv/assets/houseplan-card.js
node demo/smoke_isometric_live_touch.mjs

Результат (детерминированный, не флейк — параметры фикстуры захардкожены):

FAILED (2):
  - liveLayersPresent: expected true, got false
  - floorToOverlayOrderPreserved: expected true, got false

Причина. Фикстура (строки 24–29 файла) ставит sun.sun в {azimuth: 180, elevation: 24} и единственное окно в {type:'window', x:0.28, y:0.14, angle:0} — окно на северной стене комнаты (y=0.14, angle=0). При азимуте 180° (юг) computeSunRays() → windowLit() корректно не пропускает луч через окно, обращённое на север: .sunlayer не появляется в DOM ни в flat, ни в iso. Проверил перебором азимутов 0/45/90/135/180/225/270/315° на реальной карточке — луч есть только при 0°/45°/315° (солнце с севера), при 180° луча нет никогда. Это ожидаемое, корректное поведение существующего computeSunRays() (src/sun.ts), не регрессия из этого диапазона.

Следствие. before.sun (строка 88 смока) остаётся 0, поэтому liveLayersPresent красный сам по себе; .sunlayer не существует в DOM, поэтому ordered.every(Boolean) (строка 142) тоже красный — floorToOverlayOrderPreserved падает не потому, что порядок слоёв нарушен, а потому, что один из проверяемых узлов никогда не создаётся. Обе половины теста, отвечающие за солнечные лучи (сохранение состояния и DOM-порядок .sunlayer между flat/iso), никогда фактически не исполняются — ни на этом коммите, ни, скорее всего, ни разу с момента написания файла, поскольку per policy (AGENTS.md «During the implementation cycle only the fast gates run») браузерные смоки не запускались до этого ревью.

Почему это блокирует, а не Medium. ТЗ (089-isometric-view-stage1.md §11.6) называет именно «live-layer smoke» обязательным доказательством AC7 («room fills/hover, Glow/spill, солнце… сохраняют state… между flat/iso») и явно требует от код-ревью «выполнить HA-only update и проверить cache/ source/action parity» — то есть исполнением, не чтением. PROCESS.md §2.7: «ревьюер убедился, что тест умеет падать» — здесь тест не просто способен упасть, он уже красный на сданном коде. Раз ручного тестирования в цикле нет, красный обязательный гейт — прямое основание не принимать этап: заявленный в передаче «test: complete isometric stage 1 gates» набор гейтов не проходит целиком при реальном исполнении.

Смежная, не блокирующая часть той же находки. Чтением кода (_renderSunRays вызывается внутри того же главного <svg>, что и остальные floor-слои; геометрия луча строится в план-координатах и не зависит от _renderProjection) я убедился, что механизм солнечных лучей устройственно не привязан к проекции — при исправленной фикстуре параллель flat/iso почти наверняка подтвердится. Но это «проверено чтением», а не то же самое, что зелёный смок, и ТЗ явно требует именно исполнения для этого AC — поэтому не снижаю находку до Low/Medium на основании своего прочтения.

Рекомендация автору (не мандат, техническое решение остаётся за ним): подобрать в фикстуре либо угол/положение окна, либо азимут солнца так, чтобы computeSunRays() реально вернул луч (например, azimuth: 0 или окно на южной стене), и заново прогнать смок перед следующим циклом.

Вердикт по находке: блокирует, возврат в S6-в-разработке.

L1 — три файла с лишней пустой строкой в конце (git diff --check)

Файлы: docs/adr/089-isometric-stage1-renderer.md:106, src/iso-projection.ts:113, test/iso-projection.test.mjs:61. Серьёзность: Low.

git diff --check origin/dev...HEAD печатает «new blank line at EOF» для всех трёх. Чисто косметическая находка: не влияет на typecheck/test/build (все три гейта зелёные), не меняет поведение.

Вердикт по находке: снимается с записью — правится по желанию автора в следующем коммите этой же задачи, отдельного issue не требует.

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

  • Гейты. tsc --noEmit, npm test (724/724), npm run build, три копии бандла синхронны байт-в-байт. Commit trailers всех 6 коммитов диапазона — Issue: #89 / User-Visible: no, что верно (Labs-скрытая фича, публичного поведения нет).
  • AC1 (флаг выключен → без изменений). Подтверждено исполнением: demo/smoke_isometric_contract.mjs (flatDefault) и полный golden-прогон — все существующие плоские сцены дали passed (0 diff) против текущих эталонов; отдельный unit (isoEffectiveView) и shy-построение геометрии (_isoSceneKey() возвращает null при !this._labsIso) проверены чтением.
  • AC2–AC3 (грамматика Labs, версии, fail-closed). test/labs.test.mjs прогнан — все случаи из §2.2.1/§2.4 ТЗ (off,iso, iso,-iso, приоритет хэша, повтор параметра, malformed registry/version, недоступный storage) покрыты и зелёные. Прочитан src/labs.ts целиком — реализация буквально соответствует описанному в ТЗ алгоритму (порядок query→hash, off только «в этой позиции», window.__hpLabs — замороженный отсортированный массив).
  • AC4 (камера). ISO_CAMERA = {rotDeg:0, tiltDeg:20, xyScale:1, zScale:1, origin:[500,500]} — внутри диапазона 18–22°, совпадает с pivot NORM_W/2 из ТЗ (§4.4.3). test/iso-projection.test.mjs проверяет это утверждением на самой камере плюс round-trip на диапазоне ±5000 (docs/CANVAS.md) — прошёл. Golden-скриншот isometric-geometry-view-dark открыт и просмотрен визуально: стены не повёрнуты по диагонали, углы прямые, парные стены/колонны и дверной проём считываются корректно.
  • Математика проекции (проверено чтением + вручную). isoFloorMatrix() алгебраически выведена как афинная форма projectPlanPoint(p,0) — совпадает (тест 'floor matrix is identical to point projection' подтверждает). Важный design-факт, который делает две раздельные SVG-подсистемы (плоскость пола через preserveAspectRatio="none" на floorView, стены/маркеры через preserveAspectRatio="xMidYMid meet" на view) согласованными пиксель-в- пиксель: _applyView() всегда строит fit = fitView(vb, this._stageAspect()), то есть аспект view всегда равен аспекту контейнера, поэтому у «meet»-слоя letterbox нулевой и обе техники дают одну и ту же афинную карту. Без этого инварианта расхождение floor/walls было бы реальным дефектом — инвариант существовал до этого диапазона и не менялся.
  • AC5–AC6 (топология граней, O(E), проёмы). test/iso-walls.test.mjs прогнан (top/hole rings, full-height gap без «мостика», jamb-грани). Golden isometric-geometry-view-dark/light визуально показывают: физические стены с верх/бок гранями, virtual boundary как пунктир на полу без объёма, квадратную повёрнутую и круглую колонну с экструзией, дверной разрыв с сохранённой дугой открывания. Источник геометрии — wallBodiesGeometry(...) (тот же каноничный вызов, что использует свет, docs/LIGHT.md); physicalBodies(...) в _isoSource() даёт тот же набор тел (partitions+drafts+columns), что кэширующая обёртка _physicalBodiesR() во флэт-рендере — порядок элементов отличается, для булевого объединения это не имеет значения (сверено чтением обеих реализаций в src/physical-geometry.ts и src/houseplan-card.ts).
  • AC7 — частично. Golden isometric-live-layers-dark и isometric-no-borders-dark просмотрены: room fill/hover, Glow/spill (видимый spill через проём в соседнюю комнату), декор/мебель, устройства с бэйджами визуально присутствуют и выглядят корректно в iso; show_borders:false корректно убирает грани стен, не убирая заливку/Glow. demo/smoke_ isometric_live_touch.mjs подтвердил исполнением: flatIsoLayerParity, liveLayersStable, spillBarrierStable, haColorUpdatePainted, sameWallFingerprint, haUpdateReusesGeometry, flatIsoActionParity — все зелёные. Но солнечная часть и полная DOM-order часть этого же AC не подтверждены исполнением — см. H1.
  • AC8 (одна проекция на всё, anchor ≤1px). _scenePoint() — единственная точка входа для HTML-маркеров, room labels, vacuum trail/puck (grep по _scenePoint( в диффе — используется во всех перечисленных местах, не найдено отдельной «примерно такой же» формулы). test/isometric-contract.test.mjs проверяет это как source-contract. demo/smoke_isometric_contract.mjs (anchorsFinite, haUpdateKeepsAnchor — сдвиг ≤1px) и smoke_isometric_live_touch.mjs (orientationResizeKeepsIso) исполнены и зелёные.
  • AC9 (редакторы всегда flat). test/isometric-contract.test.mjs (doesNotMatch(spaceCard/spaceRender, …)), demo/smoke_isometric_contract.mjs (editorIsFlat, viewRestoresIso) — исполнены, зелёные.
  • AC10 (fallback-защёлка). demo/smoke_isometric_contract.mjs — инъекция исключения в _isoSource, подтверждён latch + explicit retry; вручную убедился, что тест умеет падать (см. «Мутанты» выше).
  • AC11 (перф). Профиль large-house-isometric-v1 и budgets-large-house-isometric.json добавлены по контракту существующей инфраструктуры (--budgets=, benchmark:compare), wiring в .github/workflows/performance.yml корректен. Полный 7-sample base/candidate прогон на exact-SHA Linux CI — по процессу это pre-beta гейт, не гейт этого цикла; локально подтвердил только, что раннер исполняется целиком без исключений на 1 сэмпле.
  • AC12 (backend/schema/space-card не меняются). git diff --stat подтверждает отсутствие изменений в custom_components/**/*.py, manifest.json, hacs.json; src/space-card.ts/src/space-render.ts не содержат iso-путей (unit source-contract + git diff --stat = 0 для этих файлов).
  • AC13 (без вертикальных дверей/окон). test/isometric-contract.test.mjs (doesNotMatch(card, /iso-window|window-light|vertical-door/)) и визуальный просмотр golden — дверной/оконный символ на плоскости пола сохранён.
  • AC14 (touch/kiosk). demo/smoke_isometric_live_touch.mjs — все touch/kiosk/warm-remount/orientation проверки в этом же смоке (кроме двух, см. H1) зелёные: isoOnTouch, touchPinchKeepsIso, touchOpeningIsSafe, touchLongPressHitsDevice, touchSpaceSwitchKeepsPerSpaceIso, backgroundForegroundKeepsIso, warmRemountIso, kioskReadsPreference, kioskHasNoToggle, kioskEmergencyOffIsFlat, orientationResizeKeepsIso.
  • AC15 (согласованность документов). docs/SCOPE.md содержит узкое исключение, буквально совпадающее с решением владельца O1; docs/ISOMETRIC.md, ADR, docs/DEVELOPMENT.md, AGENTS.md, docs/STATUS.md обновлены и согласованы друг с другом и с ТЗ; i18n-ключи view.volumetric/view.flat присутствуют в en.json и ru.json с формулировками из ТЗ («Объёмный вид» / «Volumetric view», «Плоский вид» / «Flat view»); README/README.ru/ docs/USER-GUIDE.ru.md/оба CHANGELOG не тронуты.
  • Отсутствие CSS 3D. test/isometric-contract.test.mjs (doesNotMatch(styles, /perspective\s*:|preserve-3d|rotateX\(|rotateZ\(/)) подтверждён; вручную прочитан весь diff src/styles.ts — новые правила (.projection-toggle, .iso-walls-svg, .iso-wall-side/top, тёмная тема) не содержат запрещённых свойств.
  • Регрессии в остальной карточке. Прогнал весь существующий браузерный смок-набор (126 файлов, не считая двух новых iso-смоков) — все 126 зелёные. Крупная интеграция (_baseVb(), _applyView(), _setMode()/mode-transition, warm-remount, _renderDevice/_renderRoomLabel/ vacuum-рендер получили новый параметр проекции) не разбила ни один существующий сценарий: zoom/pan, kiosk, warm-remount, editor gestures, opening/measure, vacuum, glow-blending и т.д. — без изменений в поведении.

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

  • Golden-эталоны для iso не принимал и не мог принять — это соответствует процессу (npm run golden:accept -- --reviewed только по полному Linux CI артефакту, PROCESS.md §12, §13 правило 13). Отсмотрел 3 из 6 захваченных actual-изображений визуально как sanity-check геометрии, не как приёмку.
  • Полный 7-sample perf-сравнение с бюджетом (budgets-large-house-isometric.json, 20% допуск) — по контракту ТЗ (§8.2) это исключительно pre-beta exact-SHA Linux CI гейт; локальный однократный прогон профиля подтвердил только «раннер работает», не «бюджет выдерживается».
  • HA-harness backend-тесты (test_ha_*.py) — не запускал: в среде нет Home Assistant и нет установленного pytest; диапазон не меняет backend, так что гейт по построению пуст, но формально не исполнялся.
  • Safari/WebKit и Firefox — ADR явно откладывает это на отдельный browser-валидационный прогон вне этого коммита; я тестировал только Chromium (тот же движок, что CI smoke/golden).
  • smoke_opening_measure.mjs — упомянутый в AGENTS.md как известно окруженчувствительный (place_dialog_x_magnetised, place_committed_x_center); в моём прогоне он тоже красным не оказался (прошёл), так что этот риск не проявился, но не является частью проверки этого диапазона.

Вердикт

Красный · цикл r2/4 · High: 1 · Medium: 0 → нет новых issue.

Один блокирующий High: обязательный по ТЗ смок demo/smoke_isometric_live_touch.mjs красный на сданном коде (H1) — AC7 не полностью подтверждён исполнением, хотя причина изолирована до ошибки в геометрии тестовой фикстуры (сон-луч), а не до дефекта рендерера. Вся остальная реализация — Labs-механизм, проекция и топология стен, кэш/fallback, warm-remount/touch/kiosk-контракт, отсутствие влияния на backend/схему/вторую карточку/публичную документацию — подтверждена исполнением (unit 724/724, 126/127 браузерных смоков, полный golden-прогон без диффа существующих сцен) либо чтением там, где исполнение не требуется по процессу. Возврат в «В разработке» для исправления фикстуры смока и повторного прогона demo/smoke_isometric_live_touch.mjs перед следующим циклом.