18 KiB
CODE-REVIEW-685-r1
Issue: #685 «Стены и штриховка становятся зубчатыми и размытыми при приближении плана»
Материал: 52fe4d6499bca1a04b32d0b4ee6054f6979458aa (ветка issue/685-static-zoom-sharpness)
Заход: r1 · блокирующих циклов израсходовано 0 из 4
Скоуп
Диапазон origin/dev..HEAD — 7 коммитов:
e8e7ecc9docs: review document for #685 (спек-ревью r1, не код)13af1d5efix: стабилизировать штриховку после зума (#685) — продуктовый коммит,User-Visible: yes139dceaa,c37b5b92,005b7c40— golden-приёмка (WSL/Linux, сBaseline-Reviewed(-Local))0a5b34a5— обновление производных гейтов (fingerprint, бюджет) после ребейза52fe4d64— fix: вернуть paint-server штриховки в defs (самокоррекция после красного полного Validate, до ревью — цикл не потрачен)
Суть исправления: терминальная (осевшая) штриховка стен на дробном масштабе ≠ 100% рисуется не масштабируемым SVG <pattern> (тайл растеризуется один раз и потом ресэмплится браузером), а аналитическим <linearGradient gradientUnits="userSpaceOnUse" spreadMethod="repeat">. На 100% сохранён байт-совместимый исторический <pattern>. Оба paint-server теперь корректно живут внутри одного <defs> (это и чинил 52fe4d64).
Как проверялось
Дешёвые гейты (tsc, npm test, npm run build) на этом SHA подтверждены зелёным Validate (run 36402254885, 36401757761) — не перегонялись повторно как самостоятельная цель, но build фактически выполнялся при каждой пересборке бандла для смоков ниже (npm run bundle:sync, тот же tsc --noEmit && rollup -c), и был зелёным во всех прогонах.
Проверено, что оба Validate-прогона на этом SHA (36401757761 push и 36402254885 workflow_dispatch с мутантами) не запускали golden/smoke/performance/geometry_parity/backend — это намеренное поведение scripts/classify-changes.mjs (heavy=false: тяжёлые job идут только на pull_request, Release:-трейлере, schedule или workflow_dispatch --full=true, которых здесь нет). Это ровно то, что процесс называет «Validate не покрывает» — прогнано вручную:
| Гейт | Команда | Результат |
|---|---|---|
| build/typecheck (для смоков) | npm run bundle:sync (= tsc --noEmit && rollup -c + sync стенда) |
зелёный, многократно |
| AC1 — терминальный кадр button/wheel/pinch | node demo/smoke_static_zoom_sharpness.mjs |
OK, все 6 проверок true |
| AC3 — регрессия плотности/угла/фазы | node demo/smoke_wall_hatch_density.mjs |
OK, все 10 проверок true |
| AC5 — перформанс (абсолютный бюджет) | node demo/benchmark_large_house.mjs --profile=large-house-interaction-v1 --samples=7 --warmups=1 --output=... + node demo/performance/compare.mjs --absolute-only --budgets=demo/performance/budgets-large-house-interaction.json |
все 51 метрика ✅ (в т.ч. interactionSeriesMs.median 3068.3 < 3300, heap.growthP95Bytes 139148 ≪ 67108864) |
| AC2 — golden (видимое изменение рендера) | npm run golden:verify (полный набор) |
188/188 passed, 0 failed, включая 4 новые сцены static-hatch-openings-{132,140}-dpr{1,2}-{light,dark} |
Бюджет бандла (INITIAL_VIEW_GZIP_CEILING, диф trai в CHANGELOG) |
node scripts/bundle-budget.mjs |
initial View: 299713 B — совпадает с числом из хендоффа и с потолком 300300±2000 |
Документация (check-docs, диф трогает src/**) |
node scripts/check-docs.mjs --external --screenshots=warn (флаг из validate.yml, не beta-кандидат) |
Documentation checks passed; единственный WARN — «screenshot fingerprint stale», ожидаемое поведение вне бета-кандидата (#479), не находка |
Смоки по выборке (smoke-select.mjs) |
node scripts/smoke-select.mjs --base origin/dev --head HEAD → 1 «зарегистрированная связь» (smoke_daycycle_raster.mjs ← renderPaperShapes, #582) + 25 «слабых» (общий символ _zoom) |
зарегистрированная связь прогнана: node demo/smoke_daycycle_raster.mjs → OK. Из слабых выбраны прогнаны напрямую названные в К5/AC4/AC5 контракты #531/#579: smoke_zoom_flash, smoke_live_pan_viewbox, smoke_live_pan_coverage, smoke_pan_any_zoom, smoke_smooth_zoom, smoke_open_passage — все OK |
| Unit (затронутые файлы) | npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test test/mutation-gate.test.mjs test/testing-doc.test.mjs test/golden-matrix.test.mjs test/wall-thickness.test.mjs |
258/258 pass (в т.ч. синхронизация docs/testing-notes/mutation-browser-guards.md со списком id в scripts/mutation-registry.mjs, версия golden-матрицы 67) |
Мутанты AC6 («защитный свидетель умеет краснеть») — проверено вручную, не только по заявлению автора. Оба зарегистрированных мутанта применены к рабочей копии, гейт перезапущен, регрессия поймана, копия восстановлена (git diff пуст, dist/ дочищен git clean -fd):
| Мутант | Патч | Guard | Результат применения |
|---|---|---|---|
hatch-stroke-not-scaled |
x2=${hatchStep} → x2=${hatchStep * 7/8} |
smoke_wall_hatch_density.mjs |
zoomDoesNotChangeThePattern: false — поймано 1/1 |
hatch-static-gradient-repeat-disabled |
spreadMethod=repeat → spreadMethod=pad |
smoke_static_zoom_sharpness.mjs |
5 проверок упали (buttonSettlesAt140, wheelSettlesAt115, pinchSettlesAt132, allModesUseSettledAnalyticHatch, reverseButtonSettlesAt140) — поймано 1/1 |
Дополнительно (не входит в реестр, проверено ради полноты защиты пятна формулы, см. находку Low-1 ниже): stroke-width=${hatchStep / 4} → stroke-width=${2} (константа) в ветке zoom===1 — smoke_wall_hatch_density.mjs тоже красит (coarseGridScalesTheStroke: false, bothRenderersAgree: false).
Формула плотности штриха алгебраически сверена построчно: старое 2 * (step / HATCH_BASE_STEP_UNITS) при HATCH_BASE_STEP_UNITS = 8 тождественно равно новому step / 4 — упрощение константы, не изменение поведения; empirически подтверждено тем же прогоном smoke_wall_hatch_density.mjs (referenceStrokeIsTwo, coarseGridScalesTheStroke).
Трейлеры (git show -s --format=%B на все 7 коммитов): Issue: #685 на каждом; User-Visible: yes только на 13af1d5e, и там же в одном коммите правки обоих docs/CHANGELOG.md/docs/CHANGELOG.ru.md — правило соблюдено. Три golden-коммита несут Release: + ровно один из Baseline-Reviewed/Baseline-Reviewed-Local, как требует §10.1.
Число, видимое дважды: INITIAL_VIEW_GZIP_CEILING = 300_300 (единственный источник — scripts/bundle-budget.mjs); фактический замер 299713 B, воспроизведён независимым прогоном bundle-budget.mjs и совпадает и с комментарием в коде, и с хендоффом автора.
Находки
Low-1 (снята без возврата)
scripts/mutation-registry.mjs: мутант hatch-stroke-not-scaled раньше целился в формулу плотности штриха ветки zoom === 1 (2 * (step / HATCH_BASE_STEP_UNITS), байт-совместимая историческая ветка, код которой в этом диффе не менялся). Диф переиспользовал тот же id для новой цели — масштабирования x2 в аналитическом градиенте (zoom !== 1). В результате для непосредственно формулы stroke-width=${hatchStep / 4} (ветка zoom===1) больше нет зарегистрированного мутанта.
Проверено, что это не оставляет реальной дыры в защите: применил ту же мутацию вручную (stroke-width=${hatchStep/4} → stroke-width=${2}) и перезапустил smoke_wall_hatch_density.mjs — тест красится (coarseGridScalesTheStroke: false, bothRenderersAgree: false). То есть смок и без формальной записи в реестре умеет ловить эту регрессию; риск чисто в трассируемости реестра, не в фактической защите. Low, правка не обязательна — фиксирую как принято с этой оговоркой, не как TODO.
Что проверено и корректно
- AC1 (
smoke_static_zoom_sharpness) — button 100→140%, обратный маршрут 196→140% (побайтно идентичный кадр,buttonHash === reverseHash), wheel 100→115%, pinch 100→132%, и все 4 режима (View/Plan/Devices/Decor) на 132% — везде точныйviewBox,transform:none,will-change:auto, аналитический градиент с ожидаемой структурой, видимые door/window/gate + модельный passage. Прогнано лично, не только по хендоффу. - AC2 (golden) — 4 новые сцены
static-hatch-openings-{132,140}-dpr{1,2}-{light,dark}(обе темы представлены, DPR 1 и 2, проблемные соседние дробные масштабы 132/140% из диагностики S2) плюс 21 обновлённая существующая сцена — весь набор 188/188 зелёный на принятых эталонах сBaseline-Reviewed-Local/Baseline-Reviewedтрейлерами. Приёмка шла через WSL/Linux-визуальную проверку по трейлерам, не только числовой порог. - AC3 — плотность/угол/фаза штриха не изменились (пруф — таблица мутантов выше и алгебраическое тождество формулы); статический рендерер (
space-render.ts) сохранил историческую<pattern>-реализацию по решению изWALL-THICKNESS.md, оба paint-server сверяются в одном смоке. - AC4 (pinch) — временный transform во время жеста допустим, после отпускания снят у сцены и у
[data-hp-live-layer="camera"]; подтверждено смоком и независимым запускомsmoke_pan_any_zoom,smoke_smooth_zoom. - AC5 (перформанс) —
large-house-interaction-v1в абсолютных бюджетах с большим запасом;smoke_zoom_flash(белые/прозрачные кадры #579) зелёный. - AC6 (защитный свидетель) — оба зарегистрированных мутанта лично применены и пойманы (таблица выше), «изменение только тестового порога» здесь не применимо — оба патча меняют продуктовую геометрию/атрибут, а не число сравнения.
- К5 (#531/#579) —
smoke_live_pan_viewbox,smoke_live_pan_coverage,smoke_daycycle_raster(зарегистрированная связь поrenderPaperShapes, порядок узла в DOM не изменился — переставлена только точка объявления переменнойwallHatch, а не позиция в шаблоне) зелёные. - Самокоррекция
52fe4d64(paint-server вне<defs>) — реальный баг был пойман собственным полным smoke автора до ревью, цикл не потрачен согласно #510; в текущем материале<defs>обёрнут корректно (проверено чтением и исполнением). - Бюджет бандла и его текстовое обоснование в
scripts/bundle-budget.mjsсовпадают с фактическим замером; единственный источник числа не задублирован. docs/ARCHITECTURE.md,docs/WALL-THICKNESS.md, обаCHANGELOG— терминология без новых пользовательских понятий (спека прямо говорит «новых настроек, сообщений и состояний нет»); UI не менялся, обращение кUSER-GUIDE.ru.mdне требовалось._wallHatchDefsвsrc/houseplan-editor-runtime.ts:5577— это точно такой же метод-дубликат (только со старой<pattern>-реализацией), но он не тронут этим диффом (git diff origin/dev...HEADдля файла пуст) и не вызывается нигде (grepпо всемsrc/*.tsне нашёл вызова ни до, ни после диффа) — предсуществующий мёртвый код, не относящийся к этой задаче; не находка этого ревью.
Чего не проверял
- Полный
npm test(3210+ тестов) не перегонял целиком отдельной командой — прогнал только пересечение с диффом (mutation-gate,testing-doc,golden-matrix,wall-thickness, 258/258 зелёных) плюс build/typecheck многократно черезbundle:sync; на весь набор полагаюсь на зелёный Validate-job «Фронтенд: типы, юниты, мутанты, синхрон бандла» на этом самом SHA. python -m pytest tests_backendне прогонял — диф не касаетсяcustom_components/**/*.py.npm run invariantsне прогонял — диф не меняет сохранённую геометрию/координаты (AC4 «Геометрическая неизменность» подтверждена таблицей мутантов и алгеброй формулы, а не инвариантами модели; геометрия стен/проёмов как данные не тронута, тронут только способ растеризации).- Изометрический (2.5D) рендер и PDF-экспорт не проверял вручную — вне скоупа задачи по её собственному разделу «Не-скоуп» («2.5D: у него отдельные материалы и нет плоской диагональной штриховки»).
- Не прогонял ручной визуальный осмотр golden-PNG глазами (кроме превью diff test output) — доверился WSL/Linux-аттестации по трейлерам
Baseline-Reviewed(-Local), что и предписывает процесс для этого гейта. - Полную матрицу
smoke-select.mjs(все 25 «слабых» связей по_zoom) не прогонял — выбрал те, что прямо покрывают контракты, названные в самом ТЗ (К5, AC4, AC5, #531/#579); остальные — общий побочный эффект символа_zoom, не специфичный для этого диффа.
Итог
High: 0. Medium: 0. Единственная находка — Low, снята без возврата автору (защита фактически подтверждена ручным прогоном, реестр — вопрос трассируемости, не корректности). AC1–AC6 доказаны исполнением, включая мутационные негативные пробы, которые я лично применил и наблюдал красный результат, а не принял на слово из хендоффа. Изменение решает заявленный сценарий (терминальная резкость на дробном зуме) без регрессии в перформансе, живом жесте, статическом рендерере или бюджете бандла.
Вердикт: зелёный.
Материал раунда
- Ветка:
issue/685-static-zoom-sharpness, коммит52fe4d6499bc— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
d9d519ccf1843466af1bdae6a822470cf2b5b555git log --all --format='%H %T' | grep d9d519ccf184 - Тело issue:
0107c8af6612487d0d3e54d481d818e69ea33110507e7b572d37038613acdd60 - Вердикт конвейера:
green· High 0