docs: review document for #689

Issue: #689
User-Visible: no
This commit is contained in:
claude[bot]
2026-09-28 17:26:30 +00:00
parent f2b66ad677
commit 4b23679d3c
+226
View File
@@ -0,0 +1,226 @@
# CODE-REVIEW-689-r1
Issue: #689 «Размытие плана после зума и белое мигание при навигации на дне/ночи (день/ночь) — откат #685»
Материал: `f2b66ad67758aa6ee391b6bb14d1f803fd93eedb` (ветка `issue/689-daycycle-layers`)
Заход: r1 · блокирующих циклов израсходовано 0 из 4
## Скоуп
Диапазон `origin/dev..HEAD` (`c1e05e46` → `f2b66ad6`), 4 коммита, 51 файл:
- `600330e1` fix: `.plan-svg` в день/ночь получает `will-change: opacity` вместо
`transform` (не фиксирует растр); `.hp-paper-outline-svg` полноразмерной карточки —
`overflow: hidden` и метка `data-hp-live-overflow="clip"`; `live-viewport.ts`
ограничивает экспозицию `[data-hp-live-viewbox]` инлайн `clip-path: inset(-25%)`,
снимаемым вместе с `overflow`; откат штриховки #685 (`linearGradient` → единственный
`<pattern>`). `User-Visible: yes`, оба CHANGELOG в этом же коммите.
- `87195c2b` — тесты/смоуки под новую композицию, `Release: v1.78.0-beta.9`.
- `836ef771` — приёмка 29 golden-кадров из Linux-артефакта CI, `Baseline-Reviewed`.
- `f2b66ad6` — отпечаток скриншотов документации (пиксели не менялись).
Продуктовые файлы: `src/styles/plan.styles.ts`, `src/live-viewport.ts`,
`src/houseplan-card.ts` (+12/-12 строк, откат штриховки внутри `render()`).
Трейлеры `Issue:`/`User-Visible:` на месте на каждом коммите; `Release:` появляется
с `Baseline-Reviewed` на golden-коммите, как требует §10.1.
## Как проверялось
**Дешёвые гейты — не перегонял, зачтён Validate на этом SHA.** Run
[36453600665](https://github.com/Matysh/houseplan-card/actions/runs/36453600665) —
`success`, но это workflow_dispatch с `heavy=false`: реально исполнились и прошли
только «Предполёт» (docs/check-docs/провенанс), «Фронтенд: типы, юниты, мутанты,
синхрон бандла» (= `tsc --noEmit`, `npm test`, `npm run build` + `bundle-policy
--verify`) и «Мутанты по диффу» (6/6). Golden/смоки/perf/geometry/backend в этом
прогоне **skipped**, не «переиспользованы» — я проверил это по логам job
«Переиспользование: это дерево уже проверено»: `smoke`/`golden` там нашли кэш-маркер
именно с `source_run=36440159452` (тот же SHA), а `performance_smoke` — «Cache not
found» (в 36440159452 перф красный, маркер успеха не пишется), и сам job
`performance_smoke` не запустился вовсе, потому что `needs.changes.outputs.heavy`
в этом workflow_dispatch оказался `false` — это НЕ доказательство перфа, только
подтверждение tsc/test/build/mutants-by-diff. Авторитетное golden+smoke evidence для
этого SHA — прогон [36440159452](https://github.com/Matysh/houseplan-card/actions/runs/36440159452)
(красный только перф, остальное зелёное).
**Гейты, прогнанные мной лично на этом SHA:**
| Гейт | Команда | Результат |
|---|---|---|
| Node-юниты по диффу | `node --test test/daycycle-layers.test.mjs test/live-viewport.test.mjs test/paper-scene-contract.test.mjs test/core-file-budget.test.mjs` | 24/24 pass; `core-file-budget`: `houseplan-card.ts` 12890 строк ≤ потолка 12891 |
| Мутанты (AC7), каждый лично применён и погашен через `mutation-gate.mjs --id=` | `daycycle-scene-transform-hint-back`, `daycycle-outline-overflow-visible`, `live-exposure-unbounded`, `live-exposure-opens-clipped-scene`, `hatch-stroke-not-scaled`, `hatch-zoom-compensation-back` | 6/6 — «заявленный тест покраснел на мутанте» |
| Смоки, названные в АС/ТЗ (после `npm run build && bundle-sync`) | `smoke_daycycle_zoom_layers`, `smoke_daycycle_layer_budget`, `smoke_static_zoom_sharpness`, `smoke_wall_hatch_density`, `smoke_daycycle_raster`, `smoke_live_pan_coverage` | 6/6 `OK`, все внутренние проверки `true` |
| `npm run golden:verify` (полный набор, не только сдвинутые) | — | **188/188 passed, 0 failed** |
| `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | — | 0 прямых/зарегистрированных совпадений, 26 «слабых» по `_zoom` (см. таблицу выше — все явно названные в ТЗ прогнаны), НЕОПРЕДЕЛЁННОСТЬ по `HATCH_BASE_STEP_UNITS`, `LIVE_SCENE_EXPOSURE_CLIP`, `SCENE_OVERFLOW_ATTRIBUTE` и др. — все они прямо покрыты новым `test/daycycle-layers.test.mjs`/`test/live-viewport.test.mjs` и смоками из строки выше |
| `git diff` полноразмерных `<pattern>`/`linearGradient` по всему дереву | `grep -rn linearGradient src demo test` | ни одного `linearGradient#hp-wall-hatch` не осталось (полный откат #685 подтверждён) |
| Docs-фингерпринт скриншотов | сверка `imageSha256` в `docs/images/screenshots.json` построчно | не изменился ни один из 11, только `sourceFingerprint`/`sourceSha256` — «pixels unchanged» подтверждено |
**Perf (AC6) — не зелёный на точном SHA, разобрано отдельно ниже.**
**Чего не проверял:** `python -m pytest tests_backend` (диф не касается
`custom_components/**/*.py`), `npm run invariants` (геометрия стен/проёмов как данные
не тронута — тронут только способ растеризации и композиция слоёв), полный `npm test`
(3210+, отдельной командой) — положился на зелёный job «Фронтенд» на этом SHA
(36440159452 и повторно 36453600665), ручную визуальную приёмку 29 golden-кадров
глазами — доверился `Baseline-Reviewed` трейлеру и Linux-артефакту CI, как
предписывает процесс для этого гейта. HA Companion и «встроенный браузер» реального
устройства не проверял — недоступно в среде ревью; для AC1/AC4 это оставлено как
визуальное свидетельство владельца (сам автор пометил их так же: «НЕ сделано»).
## По контракту и AC
- **AC1 (нет зафиксированного растра).** `test/daycycle-layers.test.mjs`,
`smoke_daycycle_zoom_layers`, `smoke_daycycle_raster`, `smoke_daycycle_layer_budget`
— везде `will-change: opacity` после осевшего кадра, `will-change: transform`
нигде не остаётся. Прочитан код: `.stage.daycycle.hp-safe-daycycle-outline
.plan-svg` (`plan.styles.ts:122`) — единственное место, где раньше стоял
`transform`. Владелец подтвердил в реальном Chrome (issue-комментарий), что
пробный CSS даёт чёткий кадр; полная бандл-версия владельцем ещё не
перепроверена (сам автор фиксирует это как «не сделано») — доказательство
контракта в CI/юнитах полное, финальная визуальная приёмка вне headless
остаётся открытой по объективной причине (сам продукт задачи).
- **AC2 (бюджет слоёв на 800%).** `smoke_daycycle_zoom_layers` (CDP LayerTree, DPR 2,
день/ночь, тёмная заливка, колесо+кнопки+pinch+перезагрузка+навигация) — прогнан
лично: `gestureLayersWithinBudget` (≤1.5× сцены), `settledLayersWithinBudget`
(≤1× сцены + 240px), `noWhiteFrames` (screencast, порог 0.25 доли почти-белых
пикселей) — все `true`. Пороги смока строже ТЗ (1.5×/1× против 2.5×/1.5×).
- **AC3 (экспозиция ограничена и снимается).** `test/live-viewport.test.mjs` — два
новых теста: инлайн `overflow: visible` + `clip-path: inset(-25%)` во время
проекции, оба свойства уходят при терминальном кадре (idle-DOM совпадает с
состоянием до жеста), сцена с `data-hp-live-overflow="clip"` никогда не
экспонируется. Прогнано лично, 2/2 pass; оба мутанта, которые снимают эту защиту
(`live-exposure-unbounded`, `live-exposure-opens-clipped-scene`), лично применены
и погашены смоком/юнитом.
- **AC4 (нет белых кадров).** `smoke_daycycle_zoom_layers.noWhiteFrames` — 0 почти-белых
кадров в центральной выборке после перезагрузки на 800% и навигации колесом.
Владелец подтвердил в реальном Chrome 152 (issue). Как и AC1, финальная
бандл-проверка владельцем на полном билде — открытый пункт по признанию автора,
не находка ревью (headless объективно не видит симптом, это прямо оговорено в ТЗ).
- **AC5 (откат #685).** `smoke_wall_hatch_density` (10/10) и `smoke_static_zoom_sharpness`
(все режимы) проверяют единственный `<pattern id="hp-wall-hatch">`; `grep` по всему
дереву не находит `linearGradient#hp-wall-hatch`. `golden:verify` 188/188, включая
21 `junction-*`/`large-house-zoom-0{40,250}` кадр, вернувшийся к виду до `139dceaa`,
и 4 `static-hatch-openings-*` с восстановленным паттерном. 4 кадра `stairs-*` из
#688 приняты тем же коммитом с явной атрибуцией и ссылкой на #690 — не скрытое
поглощение чужого дифа, а зафиксированный долг по рекомендации самого #690.
- **AC7 (защитные мутанты).** 6/6 лично погашены (таблица выше); реестр браузерных
гвардов остаётся на потолке 200/200 (одна запись удалена, одна добавлена — `grep -c`
подтверждает).
- **К6 (idle-DOM).** `smoke_live_pan_coverage` обновлён: экспонированная сцена
открывается только в рамках `clip-path`, помеченная `clip` сцена не открывается
никогда, снятие включает и `clipPath`. День/ночь golden не сдвинулись нигде, кроме
мест, которые меняет К1/К3/К5 по самому ТЗ.
- **Не-скоуп соблюдён.** `.hp-static-stage` (пространственная статичная карточка,
`space-card.ts`/`space-render.ts`) — отдельный компонент с собственными стилями;
правило `.stage .hp-paper-outline-svg { overflow: hidden }` на неё не действует
(разная специфичность/разный корневой класс, не пересекаются) — прочитано и
подтверждено, `.hp-paperg`-композиция (нетронутая карточка) не менялась.
### AC6 (перформанс) — разобрано отдельно, вердикт ревьюера: не блокирует
Точный SHA #689 на авторитетном Linux CI-прогоне
([36440159452](https://github.com/Matysh/houseplan-card/actions/runs/36440159452))
красный ровно на одной метрике из 51:
```
❌ timing.firstStableRenderMs.median 3002.4 / 3000 (+2.4 мс, +0.08 %)
```
`hardMaxMs` в `evaluate.mjs` — абсолютный потолок (`Math.min(hardMaxMs,
regressionLimit)`), не смягчается `noiseAllowanceMs`; формально AC6 не доказан
зелёным прогоном на этом SHA. Автор сам не прогнал его повторно (последующий
«зелёный» Validate 36453600665 не покрывает perf — см. «Как проверялось» выше) и
явно передал вывод ревьюеру («вывод о бюджете firstStableRenderMs после #691 — за
ревьюером»).
Я разобрал вопрос по коду и данным, а не принял решение владельца/автора на слово:
1. **Профиль не исполняет код #689.** `large-house-interaction-v1` не включает
день/ночь — `.stage.daycycle...`-правило и `.hp-paper-outline-svg`-клип не
участвуют вовсе (элемент не рендерится без `dayCycle`). Единственная
всегда-активная часть диффа — экспозиция `clip-path` в `setLayerProjection` —
исполняется во время `panSeriesMs`/`cameraSeriesMs`; оба прошли с большим
запасом (433.2/500, 199.9/500) — не похоже, что механизм экспозиции заметно
давит именно на `firstStableRenderMs` (окно, которое гейтов жестов не включает).
2. **#691, после которого впервые измерен объединённый `dev`, докс-онли** — не мог
изменить рантайм-код. Значит превышение — свойство текущего `dev` HEAD,
впервые измеренное на этом прогоне: `performance_smoke` у #691 самого не
запускался ни разу (`heavy=false` во всех 4 прогонах его ветки).
3. **Локальный A/B, прогнанный лично** (одна и та же песочница, `--samples=3
--warmups=1`, диагностика, не гейт-evidence): `dev` @ `c1e05e46` — 2936.0 /
2926.8 / 2961.1; ветка @ `f2b66ad6` — 2961.0 / 2939.4 / 2962.0. Разница между
ветками (~25 мс) того же порядка, что разброс сэмплов внутри одного `dev`
(~34 мс) — на трёх сэмплах отличить регрессию от шума нельзя.
Вывод: превышение бюджета не приписывается диффу #689 — правдоподобнее, что
`large-house-interaction-v1`/`firstStableRenderMs` уже стоит на пределе `hardMaxMs`
у текущего `dev` и это впервые проявилось именно на этом рибейзе. Это Medium-находка
вне скоупа #689 (соседний, общий perf-бюджет, а не поведение задачи) — заведена
отдельно: **[#692](https://github.com/Matysh/houseplan-card/issues/692)**
«Перф-гейт: large-house-interaction-v1 firstStableRenderMs на пределе hardMaxMs=3000
независимо от #689», с полным разбором и предложенным планом (авторитетный 7-сэмпловый
прогон `dev` HEAD, решение — расширять бюджет или чинить деградацию).
## Число, видимое дважды
`HATCH_BASE_STEP_UNITS = 8` — используется и в `wall-thickness.ts` (шаг штриховки
на эталонной клетке), и в новом `stripe = 2 * (hatchStep / HATCH_BASE_STEP_UNITS)`
(`houseplan-card.ts`). Один источник — константа импортирована, не задублирована
числом; проверено алгебраически: на `cell_cm=5` (`hatchStep=8`) `stripe = 2`, на
`cell_cm=25` (`hatchStep=1.6`) `stripe = 0.4` — совпадает с historical `stroke-width`
и с `smoke_wall_hatch_density` (`referenceStrokeIsTwo`, `coarseGridScalesTheStroke`).
## Что проверено и корректно
- Откат #685 полный и чистый: ни одного `linearGradient#hp-wall-hatch` не осталось
нигде в дереве; штриховка регрессионно идентична допо-#685 состоянию (golden
21/21 кадр вернулся к виду до `139dceaa`).
- Композиция слоёв день/ночь и статичной полноразмерной карточки читается и
исполняется согласованно с ТЗ К1–К4, К6, К7; не-скоуп (`.hp-static-stage`,
`.hp-paperg`) не задет — прочитано и подтверждено отдельно.
- Все 6 заявленных мутантов лично применены к рабочей копии и погашены
соответствующим гейтом — не принято на слово из хендоффа.
- Golden (188/188) и все явно названные в ТЗ/АС смоки лично прогнаны и зелёные.
- Трейлеры, changelog (оба языка, в одном коммите с `User-Visible: yes`), канонические
документы (`SUN.md`, `ARCHITECTURE.md`, `WALL-THICKNESS.md`) — обновлены точно там,
где меняется контракт, лишнего/недостающего не найдено.
- `core-file-budget` потолок `houseplan-card.ts` не поднят (12890 ≤ 12891) — решение
«инлайн вместо метода» не увеличивает монолит, при этом сохраняет откат #685
буквально (атрибуты паттерна, запасной цвет).
- Golden-долг #688 (`stairs-*`) явно назван в коммите, не поглощён молча — соответствует
собственной рекомендации #690 п.3.
## Находки
**Medium (вне скоупа #689) — заведён отдельный issue, не блокирует.**
- `large-house-interaction-v1`/`firstStableRenderMs` красный на точном SHA (+0.08 % над
`hardMaxMs`); по анализу выше — свойство `dev`, не этой задачи → **#692**.
High: 0. Medium в скоупе: 0. Low: 0.
## Итог
AC1–AC5, AC7 доказаны исполнением (юниты, смоки, golden, мутанты — лично прогнаны, не
приняты на веру из хендоффа); AC1/AC4 сохраняют объективно открытым только финальный
шаг «владелец смотрит на полный бандл в реальном браузере/Companion» — то же
ограничение, что и в самой задаче (headless не видит симптом), не находка ревью.
AC6 красный на точном SHA, но по независимому разбору кода и диагностическому A/B —
не является следствием диффа #689; вынесен отдельным issue #692 согласно правилу
«Medium вне скоупа». Задача решает заявленный сценарий (резкость после зума,
отсутствие мигания, откат #685) без регрессии в touch-контракте, бюджете монолита или
golden-корпусе.
**Вердикт: зелёный.**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/689-daycycle-layers`, коммит `f2b66ad67758` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `a14a6fb6b30afe13f9cf7ad1b11b343be3e8a0c6`
```
git log --all --format='%H %T' | grep a14a6fb6b30a
```
- Тело issue: `57b2345003d4b8f80b2c00dcc327b4ae2832e3de6524d1ac404eb5a32d6c84ab`
- Вердикт конвейера: `green` · High 0