Files
houseplan-card/docs/reviews/CODE-REVIEW-665-r1.md
T
2026-09-26 13:02:12 +00:00

217 lines
20 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.
# CODE-REVIEW-665-r1
Issue: [#665](https://github.com/Matysh/houseplan-card/issues/665) — «2.5D: показатели комнаты под названием «плавают» при зуме».
Этап: code-review, заход r1, лёгкий трек `small`, лимит циклов 2.
Материал: `git log --oneline origin/dev..HEAD` / `git diff origin/dev...HEAD`,
ровно
```
head: 8ffbdf8a28f89f956fe6a0e550aec7b1d42efbf5
base: 72d49040 (origin/dev)
```
Рабочая копия на этом SHA (`git rev-parse HEAD` = `8ffbdf8a28f89f956fe6a0e550aec7b1d42efbf5`), fetch/checkout не выполнялись.
## Скоуп
Два коммита:
- `c93b3a90` (fix, User-Visible: yes) — `.stage.projection-iso.mode-view .roomlabel`
теряет `min-width/min-height: 44px` и `justify-content: center`; пол
касания 44×44 переносится в `::before` (приём `.oplock::before`), новый смок
`demo/smoke_iso_room_label_metrics.mjs`, мутант
`iso-room-label-44-box-centres-name`, `docs/ISOMETRIC.md`, оба changelog.
- `8ffbdf8a` (test, User-Visible: no) — тот же смок переведён с прямой записи
`card._view` на публичные кнопки зума (`data-hp="zoom-fit"` /
`"zoom-in"`), потому что первая версия красила `no-new-private-writes`.
Класс A (`src/styles/plan.styles.ts`) + B (смок, реестр мутаций) + C (docs).
Работа закрывает J1/J5 (`docs/SCOPE.md`): показатели комнаты (температура,
свет и т. д.) в 2.5D-виде должны читаться так же надёжно, как во Flat, а не
съезжать к имени на зуме.
## Как проверялось
Валидировано, что зелёный Validate у `8ffbdf8a`
(https://github.com/Matysh/houseplan-card/actions/runs/36242169265) подтверждает
дешёвые гейты — они не перегонялись:
| Гейт | Статус | Как подтверждён |
|---|---|---|
| `tsc --noEmit`, `npm test`, `npm run build`, `bundle-policy --verify` | не перегонялся | зелёный Validate на точном SHA материала |
| `check-docs` (диф трогает `src/**`) | не перегонялся | тот же зелёный Validate (job `docs`) |
| `demo/smoke_iso_room_label_metrics.mjs` (AC1, AC2) | **прогнан здесь** | свежая сборка (`npm run bundle:sync`) → `node demo/smoke_iso_room_label_metrics.mjs` → `OK`, все чеки true |
| `demo/smoke_isometric_contract.mjs` (`raisedTargetsOwn44Pixels`, C3) | **прогнан здесь** | `OK`, `raisedTargetsOwn44Pixels: true` |
| Мутант `iso-room-label-44-box-centres-name` (AC3) | **прогнан здесь, реально, не `--check`** | `node scripts/mutation-gate.mjs --id=iso-room-label-44-box-centres-name` (без `--check`, то есть патч реально применён и гвард реально выполнен) → `ok iso-room-label-44-box-centres-name: заявленный тест покраснел на мутанте`, `поймано 1 из 1` |
| Отрицательная проба на AC2 (свой мутант, не заявленный автором) | **прогнан здесь** | вручную вырезан блок `.roomlabel::before` из `plan.styles.ts`, пересобрано → тот же смок красит `floorIs44: false`, `floorOwnsTheTarget: false`. Автор в таблице AC2 сам отметил, что не мутировал это отдельно («отрицательной пробы для «пола нет» здесь нет») — закрыто мной, третий столбец таблицы AC подтверждён исполнением, а не только присутствием `owns44` в контрактном смоке |
| `node scripts/smoke-select.mjs --base 72d49040 --head 8ffbdf8a` | прогнан, результат НЕОПРЕДЕЛЁННОСТЬ | «дифф исполняемый, но ни один смок не связан доказуемо» — решение по AC и содержанию диффа: два смока выше названы в AC/ТЗ, прогнаны прямо |
| `npm run golden:verify` (диф меняет рендер `.roomlabel` в 2.5D) | **прогнан здесь, полностью, дважды** | см. находку ниже — потребовалось сравнение HEAD vs `origin/dev` |
| `docs:accept -- --identical` / скриншоты | не перегонялся | принято по заявлению автора: диф `docs/images/screenshots.json` — только `sourceFingerprint`/`sourceSha256`, все `imageSha256` не изменились → ни один захваченный кадр документации не задет этой правкой (визуально сверено по диффу файла) |
| `python -m pytest tests_backend` | не прогонялся | диф не трогает `custom_components/**/*.py` |
| `npm run invariants` | не прогонялся | диф не трогает геометрию модели/ссылки на неё, только CSS раскладку подписи |
| perf | не прогонялся | не назван в AC, ТЗ прямо говорит «перф — нет» |
## Находки
### Medium (в скоупе задачи) — golden-влияние диффа занижено в отчёте автора вдвое-без-остатка: 0 заявлено, 8 подтверждено
**Воспроизведение.** Собрал HEAD (`npm run bundle:sync`) и прогнал
`node demo/golden/run.mjs --mode=verify` до конца (без укороченного таймаута —
предыдущая попытка с дефолтным 120-секундным лимитом обрывалась раньше
записи итогового `golden-report.json`, что само по себе стоит знать: короткий
таймаут даёт неполный список различий). Параллельно собрал `origin/dev`
(`72d49040`) в отдельном `git worktree` и прогнал тот же `golden:verify` там
для чистого сравнения «было / стало».
`origin/dev` (`72d49040`) — 12 сценариев `different`, все — известный,
задокументированный шум (`OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT` в
`demo/golden/matrix.mjs` частично их перечисляет; остальные — furniture/dialog
дрейф, тоже вне этой задачи).
HEAD (`8ffbdf8a`) — те же 12 плюс **8 новых** `different`, ни один не входит в
`OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT`:
| Сценарий | differingPixels | diffRatio | roomMetrics/label_* включены? |
|---|---|---|---|
| `isometric-geometry-view-dark` | 679 | 0.000881 | нет — только имена комнат |
| `isometric-geometry-view-light` | 679 | 0.000881 | нет |
| `isometric-touch-kiosk-dark` | 211 | 0.000740 | нет |
| `isometric-large-warm-remount-dark` | 1837 | 0.002126 | нет |
| `isometric-stage3-overlays-light` | 836 | 0.001085 | да (`stage3Fixture.roomMetrics`) |
| `isometric-stage3-overlays-dark` | 1403 | 0.001821 | да |
| `isometric-stage3-forced-colors-dark` | 3032 | 0.003934 | да |
| `isometric-stage3-no-filter-dark` | 794 | 0.001030 | да |
Визуально (`artifacts/golden/diff/*.png`) различия именно там, где и должны
быть по замыслу правки: в `isometric-stage3-overlays-dark` розовым — строка
показателей под именами комнат (сдвинулась ближе к имени, как и требует AC1);
в `isometric-geometry-view-dark` (где `label_*` не включены вовсе) розовым —
сам текст названий комнат («NW», «SW» и т. д.), сдвинувшийся на пару
пикселей, потому что якорь имени в подписи тоже зависел от снятых
`min-height`/`justify-content` (C1 в ТЗ это и требует: «положение самого имени
относительно якоря — как во Flat»).
**Почему это находка, а не просто факт жизни.** Хендофф автора говорит
буквально: «golden: 2.5D-кадров с показателями комнат в матрице не нашёл
(`dayCycle`/iso-сценарии без `label_*`), но полная матрица — на кандидате
беты». Это утверждение неверно дважды: (1) кадры с `label_*` в матрице
**есть** — четыре сценария `isometric-stage3-*`, все они используют
`stage3Fixture: { roomMetrics: true, … }` (`demo/golden/harness.mjs`
включает `label_temp/label_hum/label_lqi/label_light`); (2) реальное
воздействие правки шире, чем «кадры с показателями»: она двигает якорь
самого имени в **любой** 2.5D-подписи с `show_names: true`, что задело ещё
четыре сценария без единого включённого `label_*`. Раздел «Release-артефакты»
самого ТЗ прямо требовал этой проверки: «golden: кадры 2.5D с показателями
комнат (если есть в матрице) изменятся — приёмка по Linux CI». Кадры есть,
блок ими не ограничен, а запись в issue утверждает обратное — это ровно тот
случай, когда «не нашёл» без названной команды и её результата не
является доказательством (`AGENTS.md`, «Verified» без команды).
**Не то же самое, что дефект в реализации.** Сама правка работает верно: все
8 расхождений — маленькие (`diffRatio` 0.0007–0.0039, ниже
`maxDiffRatio: 0.0005` порога лишь на волос до полутора порядков, видимый
сдвиг на 1–3 px), согласуются с C1/AC1 и не открывают новых конфликтов
с уже проверенным C3 (`isoRaisedOverlayHalfSize` не тронут, `raisedGeometryTracksHtml`
зелёный). Обновлять сами PNG-baselines в этом коммите и нельзя: класс D,
`demo/golden/baselines/**` требует `Release:` + `Baseline-Reviewed*` трейлер,
то есть кандидат беты, не обычная задача.
**Что чинить в скоупе этой задачи (без переноса в отдельный issue — это тот
же диф, что и породил расхождение).** Автору — поправить запись в issue: ⑴
назвать восемь реально задетых сценариев (не «не нашёл»), ⑵ явно передать
эстафету: эти восемь ждут `npm run golden:accept -- --reviewed` на кандидате
беты вместе с уже известными двенадцатью, чтобы тот, кто будет принимать
кандидат, не тратил время на реатрибуцию и не принял расхождение вслепую
(`AGENTS.md`: «никогда не принимать … изображения просто чтобы CI позеленел»).
Кода это не касается — правка чисто в разделе учёта диффа.
Без High-находок это Medium в скоупе задачи → жёлтый вердикт, возврат автору
(#202): отдельный issue не заводится.
### Low, снято без возврата — расположение строки в `docs/ISOMETRIC.md`
Строка про пол подписи (`docs/ISOMETRIC.md:326-328`) добавлена в раздел
«Raised tiles (`src/iso-tiles.ts`, `src/styles/iso-tiles.styles.ts`)», хотя
подписи комнат рендерятся из `src/styles/plan.styles.ts` и вообще не через
`iso-tiles.ts`. Спек-ревью уже отмечало этот пункт как Low и оставляло на
усмотрение исполнителя — здесь тоже не блокирует ни один AC (сама формулировка
верна, просто не в том разделе). Оставляю на усмотрение автора при следующей
правке этого файла; отдельного действия не требую.
## Что проверено и подтверждено корректным
- **Причина бага и её устранение** — построчно совпадают с тем, что зафиксировал
спек-ревью: `min-height: 44px` + `justify-content: center` на flex-контейнере
подписи центрировали имя в коробке 44 px, `.rlmetrics` (`position: absolute;
top: calc(100% + 0.15em)`) отсчитывалась от нижней границы этой коробки, а не
от имени — отсюда зависящий от зума зазор. Правка снимает оба свойства и
переносит пол 44×44 в `::before` с `z-index: -1` **внутри собственного
контекста наложения `.roomlabel`** (`position: absolute; z-index: 1` на самой
подписи, `src/styles/plan.styles.ts:557,565`) — это не мировой z-index, а
локальный порядок внутри одной подписи, поэтому пол рисуется под текстом
подписи, но не проваливается под соседние элементы плана. Приём — буквальная
копия `.oplock::before` (`src/styles/plan.styles.ts:541-551`), с тем же
`width/height: max(44px, 100%)` и центрированием `translate(-50%, -50%)`.
- **AC1** — доказан исполнением: `demo/smoke_iso_room_label_metrics.mjs`,
свежая сборка, зелёный. Отношение `gap/высота имени` совпадает в Flat и 2.5D
на обоих зумах (0.096 и 0.098 в паре точек, разница < 0.02) и не плывёт между
зумами — это ровно то же самое соотношение из таблицы issue (Flat: 0.1
постоянно, было 2.5D: 2.0 → 0.16, стало 2.5D: то же 0.1, что и Flat).
- **AC2** — доказан исполнением дважды: положительно (`floorIs44`,
`floorOwnsTheTarget`, `areaLinkStaysClickable` — все true) и отрицательно
(ручное снятие `::before` красит именно эти два чека, проверено мной, не
автором — см. таблицу гейтов выше).
- **AC3** — мутант `iso-room-label-44-box-centres-name` реально пойман
(`mutation-gate.mjs` без `--check`, не только статическая проверка якоря).
- **C3** — `isoRaisedOverlayHalfSize` (`src/iso-scene-render.ts`) не тронут
этим диффом (диф ограничен `plan.styles.ts`); `smoke_isometric_contract`
зелёный целиком, включая `raisedTargetsOwn44Pixels`; `houseplan-space-card`
никогда не рендерит класс `projection-iso` (`grep` по `src/space-card.ts` —
пусто), поэтому не мог быть задет структурно, что независимо подтверждает,
что все восемь золотых расхождений — ожидаемый и единственный побочный эффект
этой правки, а не что-то посевное.
- **Трейлеры** — `Issue:`/`User-Visible:` на обоих коммитах корректны; `yes`
на `c93b3a90` сопровождается правкой обоих changelog в том же коммите;
`no` на `8ffbdf8a` — тестовая правка без изменения продукта, changelog не
трогает, верно.
- **Одно число — один источник.** Пиксельный литерал `44px` в новом
`::before` не создаёт второй источник истины: это тот же паттерн, что уже
используют `.oplock::before` и `.dev` (`src/styles/devices.styles.ts:181-182`)
— общей CSS-переменной для 44 px в кодовой базе нет нигде, буквальное
повторение литерала — существующая конвенция, не регресс.
- **`no-new-private-writes`** — второй коммит специально существует, чтобы
исправить эту находку CI; проверил, что `demo/smoke_iso_room_label_metrics.mjs`
больше не пишет `card._view` напрямую, а жмёт `data-hp="zoom-fit"`/`"zoom-in"`;
единственная мутация состояния конфигурации — `space.settings = {...}` на
локальной переменной `space`, не в цепочке с `_`-полем карточки, гейт бы её
не поймал и это ожидаемо.
## Чего не проверял
- Полную матрицу смоков (277 файлов) — прогнаны только два, названных AC, и
контрактный `smoke_isometric_contract`; остальные — предрелизная
обязанность.
- `pytest tests_backend` — диф не трогает Python.
- `npm run invariants` — диф не трогает геометрию модели.
- Performance — не назван в AC, ТЗ говорит «перф — нет» (одно CSS-правило,
довод принят без отдельного прогона).
- Скриншоты документации визуально (только по `imageSha256` в диффе
`screenshots.json` — не совпавших нет, поэтому не открывал сами PNG).
- Полный `golden:capture`/`accept` — не входит в обязанности код-ревью
(класс D требует кандидата беты); прогнан только `verify` (диагностика).
## Вердикт
Единственная блокирующая находка — Medium в скоупе задачи, без High.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/665-iso-label-metrics`, коммит `8ffbdf8a28f8` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `564583c27e4579c22778936761a60ee40306774c`
```
git log --all --format='%H %T' | grep 564583c27e45
```
- Тело issue: `8da493f662bb94963a92e6ca40c224f16235947efbd137e809e313833e01cc97`
- Вердикт конвейера: `yellow` · High 0