mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
docs: code review document for #122 (r1, red)
Issue: #122 User-Visible: no
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
# Код-ревью #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`.
|
||||
Reference in New Issue
Block a user