Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности (`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` → `legacy/specs/`), а относительные ссылки внутри перенесённых документов и в соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно только до переноса документов ревью). Гейты архив не смотрят. `reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не резолвится от нового места, а цель находится от нового или старого места через карту переносов; битая и до переноса ссылка не трогается. `--apply` делает это само, `--repair-links=<rev>` — для всех переименований `<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом `--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты: перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет; мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и DEVELOPMENT › Release называют переписывание и `--check-links`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
27 KiB
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. - Мутант про инъекцию рендер-исключения (
_isoSourcethrows) —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°, совпадает с pivotNORM_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-грани). Goldenisometric-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\(/)) подтверждён; вручную прочитан весь diffsrc/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 перед следующим циклом.