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

249 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Код-ревью #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`.