Files
houseplan-card/docs/reviews/CODE-REVIEW-532-r1.md
T
2026-09-11 17:01:04 +00:00

208 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.
# CODE-REVIEW-532-r1
Вердикт: **жёлтый** · заход r1 · блокирующих циклов израсходовано 0 из 4 · High: 0 · Medium: 2 (в скоупе, возврат автору)
Материал: `bb04f8605f9003c0ec6643359fa47af9319247bc` (ветка приведена к `dev`
конвейером; `origin/dev..HEAD` содержит один коммит, диплист диффа полный, не по
дельте — цикл первый).
## Скоуп
Issue #532: тройной `drop-shadow` внешнего контура плана (`.hp-paperg`) живёт в
общем композиционном слое с планом, поэтому любая перерисовка плана (ховер,
панорама) заново прогоняет три прохода размытия по габариту всего листа —
на машине владельца (Firefox) это ~9 к/с и 23 МБ текстур/кадр. Правка:
`will-change: filter` на правиле `.stage.daycycle .hp-paperg,
.hp-static-stage.daycycle .hp-paperg`, уводящее отфильтрованную бумагу в свой
слой. Побочный эффект контракта (К2, принят на спек-ревью r2): промоушен делит
SVG на два слоя, из-за чего диагональная штриховка стен антиалиасится иначе —
4 golden-кадра `day-cycle-{dawn,day,dusk,night}-dark` расходятся и подлежат
пересъёмке на кандидате беты, а не в этом коммите.
Продуктовая рамка: View-режим — продукт для двух из трёх персон (`SCOPE.md`),
настенный планшет и телефон читают план именно там; ускорение растеризации
дневного цикла закрывает именно этот сценарий, палитру/геометрию/слои
окружения/блендинг не трогает (К3, К4 — вне скоупа, подтверждено кодом, см.
ниже). Продукт кода вне `plan.styles.ts` не тронут.
Спек-ревью прошло два раунда (r1, r2 — оба зелёные); r2 переписал К2/AC3 после
того, как автор сам обнаружил на реализации, что «golden останется зелёным» не
подтвердилось, и вернул ТЗ на переработку вместо подгонки факта под старый
текст. Код-ревью (этот документ) — первый заход, полный разбор.
## Как проверялось
Дешёвые гейты (`tsc`, `npm test`, `npm run build` + сверка 3 копий бандла,
`check-docs.mjs`) подтверждены зелёным прогоном Validate на этом самом SHA
(https://github.com/Matysh/houseplan-card/actions/runs/34622197915) — повторно
не гонял, кроме случаев ниже, где перегон был нужен как побочный эффект.
Гейты, которые я прогнал сам (а не поверил хендоффу):
| Гейт | Команда | Результат |
|---|---|---|
| Смок-свидетель | `node demo/smoke_daycycle_raster.mjs` (после `npm run bundle:sync`, локальный ratio 0.73 — другая машина, тот же порядок, что у автора 0.76/0.82) | OK, 8/8 |
| Мутант | `node scripts/mutation-gate.mjs --id=daycycle-outline-not-promoted` | `тест покраснел, как обязан` — поймано 1 из 1 |
| Селектор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | НЕОПРЕДЕЛЁННОСТЬ (0 символов на изменённой строке) — совпадает с заявленным риском 5 |
| Полная golden-матрица | `node demo/golden/run.mjs --mode=verify` (169 сценариев) | ровно 4 `different`: `day-cycle-{dawn,day,dusk,night}-dark`; всё остальное `passed`, включая 7 сценариев, которые в хендоффе автора были помечены как «шум песочницы» — здесь (пиновый Chromium) они все зелёные |
| Мой собственный замер среднего цвета кадра по всем 4 кадрам (не только `night-dark`, как в хендоффе) | одноразовый скрипт через `createImageBitmap`/`getImageData`, baseline vs `artifacts/golden/actual/*`, удалён после прогона | `dawn` Δ−0.001, `day` Δ−0.014, `dusk` Δ+0.047, `night` Δ+0.080 из 255 — все четыре внутри заявленного допуска «не дальше 0.1» |
| Ограничивающий прямоугольник расхождения по всем 4 кадрам | тот же одноразовый прогон | `[120‑121, 36, 867, 757‑758]` — идентичен по всем четырём кадрам и совпадает с рамкой, которую автор привёл для `night-dark`; расхождение не задевает ореол снаружи плана |
| Три копии бандла | `diff -q dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` (+ `houseplan-assets.json`) | идентичны |
Не прогонял: `pytest tests_backend` (бэкенд не задет), `npm run invariants`
(диффа геометрии/модели/`layout`/`marker.space`/`open_spans` нет — правка
чисто в CSS), performance-профили Validate (не названы в AC), полный
`npm test`/`tsc`/`build` заново как отдельный гейт (уже зелёные на этом SHA
по Validate; `npm run build`+`bundle:sync` я всё равно выполнил как побочный
эффект, чтобы поднять локальный смок/golden — красных мест не дали).
`smoke_glow*`/`smoke_discovery_filters` не гонял: слабое совпадение по имени
(«свет», «фильтр»), но К3/К4 прямо объявляют слои окружения и блендинг вне
скоупа, а код-факт (`svgScreenBlendSupported` не читает `bg_mode`) это
подтверждает — смотреть не было необходимости.
Рабочая копия оставлена чистой: все временные скрипты и `artifacts/golden/`
удалены после использования, `git status` пуст.
## AC · чем доказан · чем краснеет (проверено самостоятельно)
| # | AC | Чем доказан | Чем краснеет | Проверено |
|---|---|---|---|---|
| AC1 | Подсказка `will-change: filter` стоит только при `daynight`, статичный фон — без неё | `demo/smoke_daycycle_raster.mjs`, часть 1 | мутант `daycycle-outline-not-promoted` | сам прогнал: смок зелёный на коде, красный на мутанте |
| AC2 | Отношение растеризации на панораме ≤ 2.0 | тот же смок, часть 2 (CDP `RasterTask`) | тот же мутант (без декларации отношение ~15) | сам прогнал: 0.73 на коде, мутант красный |
| AC3 | Изменились ровно 4 кадра `day-cycle-*-dark`; расхождение — антиалиасинг штриховки, не изменение вида (ореол цел, средний цвет кадра сдвинут ≤0.1/255) | полная golden-матрица + разбор пикселей | 5-й кадр либо сдвиг среднего цвета | сам прогнал полную матрицу (169 сценариев): ровно 4 different; сам посчитал средний цвет и bbox расхождения для **всех четырёх** кадров (не только `night-dark`, как в хендоффе) — все внутри допуска, bbox идентичен по всем четырём |
| AC4 | Статичный фон не платит: ни `filter`, ни `will-change` | тот же смок | декларация без `.daycycle` | сам прогнал вместе с AC1 |
| AC5 | `docs/SUN.md` описывает промоушен как часть контракта; оба чейнджлога на месте | построчная сверка ревьюером | расхождение документа и кода | сверил построчно — обе строки чейнджлога и абзац SUN.md на месте, но см. находки ниже |
AC3 закрыт по существу мной лично, а не на веру: спек-ревью r2 прямо
отметило, что автор экстраполировал порог сдвига цвета с одного разобранного
кадра (`night-dark`) на оставшиеся три и делегировало код-ревью обязанность
проверить это «на реальном прогоне». В хендоффе автора эта проверка
по-прежнему покрывала только `night-dark` количественно (три другие только
помечены `different` в таблице). Я прогнал полную матрицу и лично посчитал
средний цвет и bbox расхождения для `dawn`/`day`/`dusk`-dark — все три ведут
себя идентично `night-dark` (тот же bbox, тот же порядок сдвига цвета), так
что риск экстраполяции снят фактом, а не доверием.
## Находки
### Medium-1 (в скоупе) — чейнджлог утверждает то, что опровергает сам контракт задачи
`docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` (новая запись в `## Unreleased`):
> The picture is unchanged, to the pixel ([#532]).
> Картинка не изменилась ни на пиксель ([#532]).
Это буквально неверно и противоречит К2/AC3 этой же задачи: 3.3–3.5 %
пикселей на каждом из четырёх кадров дневного цикла расходятся (максимальное
отклонение канала 81 из 255) — именно поэтому эталоны `day-cycle-*-dark`
пересматриваются отдельным коммитом на кандидате беты. Сообщение самого
коммита формулирует это точнее («the diagonal wall hatch anti-aliases
differently and the four day-cycle baselines are re-taken»), а публичный
чейнджлог — нет.
**Воспроизведение:** сравнить текст `docs/CHANGELOG.md` с любым из четырёх
диффов golden-матрицы (`node demo/golden/run.mjs --mode=verify`,
`day-cycle-night-dark`: 26 442 / 770 640 пикселей выше `maxChannelDelta: 10`).
**Правка:** перефразировать обе строки чейнджлога так, чтобы не утверждать
побайтовую идентичность — например, в духе текста коммита («картинка читается
так же; антиалиасинг штриховки стен смещается на несколько пикселей, эталоны
дневного цикла пересматриваются отдельно, см. #532»).
### Medium-2 (в скоупе) — `docs/SUN.md` приписывает измерение в Chromium браузеру Firefox
`docs/SUN.md`, новый абзац (AC5):
> …without the hint every repaint of the plan re-ran three blur passes over
> the whole sheet, which cost **a Firefox window** about **fifteen times**
> the rasterization of a static background (#532).
Отношение «пятнадцать раз» — это измерение мутанта/смока через CDP-трассировку
в headless **Chromium** (см. хендофф: «без правки… отношение 15.06»; тот же
порядок в моём собственном прогоне мутанта). Профиль владельца в Firefox дал
качественные цифры (23 МБ текстур/кадр, ~9 к/с, вклад drop-shadow 72–75 % от
стоимости растеризации), но никогда не давал коэффициент «×15» — сам документ
ТЗ и ревью r1/r2 явно фиксируют, что Gecko не профилировался этим свидетелем
(«Chromium — не Gecko… приговор по Gecko даёт профиль владельца»). Формулировка
SUN.md смешивает два источника числа под одной меткой, что противоречит
собственной дисциплине проекта «одно число — один источник».
**Воспроизведение:** сверить абзац SUN.md с хендоффом/комментарием аналитики —
цифра 15× нигде не связана с Firefox, только с Chromium/CDP.
**Правка:** заменить «a Firefox window» на «our Chromium/CI measurement» (или
эквивалент), не приписывая коэффициент профилю владельца.
## Что проверено и корректно
- Селектор и объявление `will-change: filter` в `src/styles/plan.styles.ts`
дословно совпадают с К1 (в т.ч. вторая ветка селектора `.hp-static-stage`);
комментарий над правилом даёт числа Chromium/CDP корректно (852 мс / 28 мс /
108 мс) — в отличие от SUN.md, здесь атрибуция источника числа верна.
- `.hp-paperg` действительно группа только бумажных силуэтов
(`src/houseplan-card.ts:11534`, `_paperShapes(space.rooms)`), не меняется
при ховере/панораме — механика из ТЗ подтверждена чтением, не только словом
аналитика.
- `scripts/mutation-gate.mjs`: патч `daycycle-outline-not-promoted` находит
ровно ту строку, которая добавлена в диффе (сверено побайтово через `grep`),
мутация подтверждена прогоном.
- `demo/golden/matrix.mjs` содержит ровно 4 сценария с `bgMode: 'daynight'` —
структурно подтверждает и «К2 живёт только под `.daycycle`», и «ровно четыре
кадра» из AC3.
- `svgScreenBlendSupported` (`src/glow-blend.ts`) — рантайм-проба, не читающая
`bg_mode` — подтверждает К4 (блендинг света вне скоупа) кодом.
- `docs/images/screenshots.json`: изменился только `sourceFingerprint`
(отпечаток `src/**`), все 11 `imageSha256` не изменились — соответствует
заявлению «`npm run docs:accept -- --identical`», документация не поехала.
- Три копии бандла (`dist/`, `custom_components/houseplan/frontend/`)
побайтово идентичны; сборка воспроизводится (`npm run build` +
`bundle:sync` на этом дереве прошли чисто).
- Коммит несёт `Issue: #532` и `User-Visible: yes`; обе строки чейнджлога — в
том же коммите, что и поведение (не в отдельном).
- Продуктовый вопрос автора («приемлемо ли платить пересъёмкой 4 эталонов за
скорость») не новый: это ровно К2/AC3, уже разобранные и принятые зелёным
спек-ревью r2 с полным раскрытием факта (byte-for-byte ореол, сдвиг
среднего цвета, план пересъёмки с `Release:`/`Baseline-Reviewed:`). Код
реализует то, что было одобрено; переоткрывать это как продуктовое решение
на этапе код-ревью нет оснований — я лишь перепроверил цифру на всех
четырёх кадрах вместо одного (см. AC3 выше).
- Риск 5 из ТЗ (`smoke-select` не находит связь) подтверждён независимо:
тот же вывод «НЕОПРЕДЕЛЁННОСТЬ» на моём прогоне, что и в хендоффе.
## Чего не проверял
- `pytest tests_backend` — диффа в `custom_components/**/*.py` нет.
- `npm run invariants` — диффа геометрии/модели (рёбра, `layout`,
`marker.space`, `open_spans`) нет, только CSS-свойство.
- Performance-профили Validate (`performance_smoke` и т.п.) — не названы в
AC, диффа в этих путях нет.
- Приговор по настоящему Firefox (профиль владельца до/после) — вне
возможностей код-ревью; ТЗ явно оставляет это отдельным шагом суждения
владельца, а не AC этой задачи.
- Полный `smoke_*` матрица (244 файла) — не прогонял целиком; `smoke-select`
честно вернул «неопределённость», просмотрел список смоков с похожими
именами (`glow*`, `discovery_filters`) и решил не гонять — К3/К4 прямо
выводят эти поверхности из скоупа, и код (`svgScreenBlendSupported`) это
подтверждает.
---
**Материал раунда:** SHA `bb04f8605f9003c0ec6643359fa47af9319247bc`,
`origin/dev..HEAD` = 1 коммит, диапазон диффа `origin/dev...HEAD` (53 файла,
включая 3 копии бандла и golden-fingerprint документации). Первый заход —
раздела «Унаследовано из r0» нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/532-daycycle-raster`, коммит `bb04f8605f90` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `959371418fa5d512b6314804fb4d9cf13e86efe4`
```
git log --all --format='%H %T' | grep 959371418fa5
```
- Тело issue: `12e66588332b94c78100156f01e4832881b8163fb6f8fdca83acfcb8e90f4545`
- Вердикт конвейера: `yellow` · High 0