mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-03 21:28:59 +00:00
249 lines
19 KiB
Markdown
249 lines
19 KiB
Markdown
# Код-ревью #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`.
|