Files
2026-09-26 13:02:12 +00:00

20 KiB
Raw Permalink Blame History

CODE-REVIEW-665-r1

Issue: #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.


Материал раунда

  • Ветка: issue/665-iso-label-metrics, коммит 8ffbdf8a28f8 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 564583c27e4579c22778936761a60ee40306774c
    git log --all --format='%H %T' | grep 564583c27e45
    
  • Тело issue: 8da493f662bb94963a92e6ca40c224f16235947efbd137e809e313833e01cc97
  • Вердикт конвейера: yellow · High 0