From 4b23679d3cbf1607a2e41a760cfce5c773cb0000 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:26:30 +0000 Subject: [PATCH] docs: review document for #689 Issue: #689 User-Visible: no --- docs/reviews/CODE-REVIEW-689-r1.md | 226 +++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/reviews/CODE-REVIEW-689-r1.md diff --git a/docs/reviews/CODE-REVIEW-689-r1.md b/docs/reviews/CODE-REVIEW-689-r1.md new file mode 100644 index 00000000..efe610db --- /dev/null +++ b/docs/reviews/CODE-REVIEW-689-r1.md @@ -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` → единственный + ``). `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` полноразмерных ``/`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` + (все режимы) проверяют единственный ``; `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-корпусе. + +**Вердикт: зелёный.** + +--- + + + +## Материал раунда + +- Ветка: `issue/689-daycycle-layers`, коммит `f2b66ad67758` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. +- Дерево материала: `a14a6fb6b30afe13f9cf7ad1b11b343be3e8a0c6` + ``` + git log --all --format='%H %T' | grep a14a6fb6b30a + ``` +- Тело issue: `57b2345003d4b8f80b2c00dcc327b4ae2832e3de6524d1ac404eb5a32d6c84ab` +- Вердикт конвейера: `green` · High 0