24 KiB
CODE-REVIEW-683-r1
Issue: #683 — «Лестницы: собственные цвета, трапеция направления и равномерный
шаг ступеней».
Этап: code (код-ревью, PROCESS.md §2.7).
Заход: r1 · блокирующих циклов израсходовано 0 из 4.
Материал: git log --oneline origin/dev..HEAD и git diff origin/dev...HEAD,
ровно SHA 20d2863eea3f94a83269bf8c2ba460a81ebdb41f (рабочая копия уже на нём;
git rev-parse HEAD сверен непосредственно перед подведением итогов).
Скоуп
Задача обслуживает J1 («что происходит сейчас» — понятный символ, не
завязанный на цвет темы HA) и J4/J6 («GUI без ручной правки конфига» — два
стандартных hp-color-opacity, snapshot цвета декора) из docs/SCOPE.md.
Видимое поведение меняется: User-Visible: yes верен, оба CHANGELOG обновлены
в одном коммите (5b5b8081).
Диапазон коммитов: 5b5b8081 (feat, продуктовый код + документация +
changelog), 64b544f1/2c9ff32d (chore(golden), с Release: и
Baseline-Reviewed-Local: трейлерами), 1a7b1dfc (fix: манифест
scripts/config-schema.json), 20d2863e (docs: fingerprint скриншотов).
Трейлеры Issue:/User-Visible: присутствуют на каждом коммите; на
коммитах, трогающих demo/golden/baselines/**, есть ровно один
Baseline-Reviewed-Local: плюс Release: — по правилу.
Как проверялось
Прочитаны: docs/SCOPE.md, docs/process/REVIEWER.md, AGENTS.md, тело
issue #683 (ТЗ r1, все 17 разделов, AC1–AC14), docs/STAIRS.md. Разобран
полный git diff origin/dev...HEAD (39 файлов) построчно: src/stairs.ts
(модель, stairVisualStyle/stairVisualFields/stairStyleVars,
stairIntervalCount, геометрия трапеции/ступеней, renderFingerprint),
src/stairs-editor.ts, src/stairs-editor-model.ts, src/stairs-box.ts,
src/stairs-view.ts, src/space-render.ts, src/styles/plan.styles.ts,
src/pdf/pdf-scene.ts, backend (validation.py, import_export.py,
support_package.py), i18n × 4, docs/ARCHITECTURE.md,
docs/CONFIG-COMPATIBILITY.md, оба USER-GUIDE, scripts/bundle-budget.mjs,
scripts/config-schema.json, scripts/mutation-registry.mjs,
scripts/monolith-baseline.json, demo/golden/baselines/*,
demo/smoke_stairs.mjs, test/stairs.test.mjs, test/pdf-scene.test.mjs,
backend тесты.
Таблица AC — доказательство и что я сделал сам
| AC | Заявлено | Проверено как |
|---|---|---|
| AC1 | snapshot цвета/opacity/fill_opacity=0, независимость от будущего decor default | unit (stairs.test.mjs) + smoke (newStairsSnapshotCurrentDecorStyle, legacyReadDoesNotMaterializeStyle) — прогнано мной, все true |
| AC2 | два color picker, Save/Cancel/reopen, смена типа сохраняет | smoke (propertiesExposeBothColourControls, cancelKeepsStairVisualStyle, reopenRestoresPersistedStyle, kindSwitchKeepsIdentityAndLink) — прогнано мной |
| AC3 | цвет на outline/trapezoid/treads/arrow, fill на весь rect/circle, hover/selected — только временный outline | unit DOM (CSS в plan.styles.ts, прочитано) + golden (я прогнал golden:verify, 184/184) |
| AC4 | трапеция 100/80, отступы 10%, forward/«Вверх» 80→100, стрелка одинакова | unit geometry (stairs.test.mjs, прогнано: backward.arrowPath === geometry.arrowPath) + golden |
| AC5 | treads строго между боковыми сторонами, широкое основание не дублируется | unit (trapezoid.length === 3, прогнано) + golden |
| AC6 | N минимизирует |L/N−30см|, tie → большее N, resize повторяем | unit stairIntervalCount — прогнано (288→10, 40→2 с комментарием «tie», 0→1) |
| AC7 | «Вверх/Вниз» на 4 языках, spiral не меняется | unit i18n (i18n.test.mjs, прогнано, 27/27) + smoke |
| AC8 | legacy fallback без записи, backend отклоняет плохие color/opacity | frontend unit (stairVisualStyle не мутирует legacy-объект, прогнано) + backend (test_validation.py, читано; 4 новых broken-кейса на color/opacity/fill_color/fill_opacity) |
| AC9 | backup/import/export/copy/support сохраняют безопасные поля | backend (test_ha_import_export.py, test_support_package.py, читано: unknown-поле future_stair/unknown подтверждённо отброшено, цветовые сохранены) |
| AC10 | footprint/bounds/magnet/hit target не зависят от trapezoid | проверено чтением: stairFootprintGeometry/stairOutline не используют trapezoid; regression-тест #663 stair magnet … не тронут диффом |
| AC11 | одна geometry для View/editor/2.5D/PDF, PDF монохромный | проверено чтением: PDF и View/editor вызывают один и тот же cachedStairRenderGeometry; unit (pdf-scene.test.mjs, прогнано: styledOutput.commands идентичен plainOutput.commands) |
| AC12 | только active получает cursor pointer | smoke (getComputedStyle(linkedNode).cursor === 'pointer', прогнано) + regression target-state тесты (не тронуты) |
| AC13 | light/dark: transparent default, пользовательские цвета, straight-направления, spiral, без theme-подмены | golden — см. находку M1: пользовательские цвета в матрице не представлены |
| AC14 | кеш геометрии не зависит от стиля; 250 лестниц в бюджете | unit (cachedStairRenderGeometry — правка цвета не инвалидирует кеш, прогнано) + scripts/model-invariants.mjs --config на экспорте makeLargeHouseFixture (250 лестниц) — прогнано мной, «Инварианты выполнены» |
Гейты — что прогнал я сам и почему
CI Validate на точном SHA (20d2863e) я не считаю панацеей автоматически —
проверил, что именно выполнилось, а не пропущено через reuse-механизм:
| Гейт | Статус | Источник |
|---|---|---|
npx tsc --noEmit, npm run build, unit, bundle-sync |
зелёные (реально исполнены) | run 36347331581 job «Фронтенд: типы, юниты, мутанты, синхрон бандла» — success; я также сам прогнал npm run build локально — успех |
Мутанты по диффу (6/6 шардов, включая 3 новых свидетеля #683: stairs-tread-count-depends-on-render-scale, stairs-legacy-save-keeps-implicit-style, stairs-active-link-loses-pointer-cursor) |
зелёные, реально исполнены | run 36347608885 — все 6 шардов success (не skipped) |
| Backend pytest с полным HA harness | зелёный, реально исполнен (не reuse) | run 36346278615, SHA 99569753 (backend-релевантные файлы не менялись после этого коммита при ребейзе — сверено диффом) — job «Бэкенд: pytest в Home Assistant» success |
npm run golden:verify (полная матрица, 184 сцены — --only не поддержан CLI, флаг был проигнорирован и прогнал всё) |
прогнал сам, exit 0, все сцены passed, ни одной ошибки/расхождения |
локально, эта сессия |
node demo/smoke_stairs.mjs |
прогнал сам, 62/62 проверки true, OK |
локально, эта сессия |
node scripts/model-invariants.mjs --config <экспорт> на demo/fixtures/large-house.mjs (250 лестниц, AC14) |
прогнал сам, «Инварианты выполнены: ссылки разрешимы, записи толщины находятся» | локально, эта сессия |
node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прогнал сам: 22 прямых совпадения, из них ровно один осмысленный (demo/smoke_stairs.mjs) — остальные 21 совпали по широким символам (_decorStyle, cellCm, NORM_W, GRID_N), общим для десятков несвязанных смоков; не прогонял их отдельно — риск для лестниц там нулевой |
локально, эта сессия |
test/i18n.test.mjs, test/stairs.test.mjs, test/pdf-scene.test.mjs |
прогнал сам | локально: 27/27, 43/43, все pass |
python -m pytest tests_backend |
не прогнал — в этом окружении нет .venv-backend/homeassistant; полагаюсь на CI-прогон с реальным harness выше, а не на голый pytest (он бы скипнул test_ha_*.py и ничего не доказал, docs/TESTING.md) |
— |
npm run golden:capture/full performance suite |
не прогонял — предрелизный объём, не требуется гейтом ревью (§8) | — |
Находки
M1 (Medium, в скоупе) — AC13 не показывает «пользовательские цвета» в golden
demo/golden/matrix.mjs:271-282 определяет ровно 4 сцены лестниц через один и
тот же фикстур stairLayerFixture (demo/golden/matrix.mjs:210-223). Ни один
из четырёх объектов там не задаёт color/opacity/fill_color/
fill_opacity — все они рендерятся с fallback-значением decor-default
(#607d8b, fillOpacity: 0). Я проверил: во всём demo/golden/matrix.mjs и
demo/golden/harness.mjs нет ни одного места, где стул задаёт кастомный
цвет/заливку для лестницы.
AC13 в теле issue дословно требует: «Светлая и тёмная темы показывают
transparent default, пользовательские цвета, оба straight-направления и
spiral без theme-dependent подмены» с единственным заявленным способом
доказательства — golden. Фактически принятая матрица показывает только
default-стиль (транспарентная заливка, дефолтный цвет) во всех четырёх темах/
режимах; отдельного кастомного (не-default) цвета/прозрачности лестницы в
golden нет вообще.
Прецедент в этом же фикстуре показывает, что проект такую ось считает нужной:
geo-axis-h (decor-линия в том же golden-geometry, demo/fixtures/ visual-matrix.mjs:88-89) намеренно несёт явный нестандартный color/
opacity, отличный от темы, и это видно в golden. У лестниц аналога нет.
Смок (coloursAndOpacityPersistAsOneEdit в demo/smoke_stairs.mjs, я
прогнал — true) подтверждает, что кастомный цвет реально долетает до
--hp-stair-line/--hp-stair-fill в DOM, и CSS-правило, которое их
потребляет (plan.styles.ts:1567-1585), — тривиальный var()-проброс,
уже используемый для декора в других местах. Поэтому фактический
визуальный риск невелик — но заявленное доказательство (golden) для этой
конкретной фразы AC13 отсутствует, а не «ослаблено».
Чем краснеет: пусто (defensive-таблица не заведена для этого пункта,
хотя AC13 — не защитный AC, а описательный про рендер; тем не менее
раздел §8/§2.7 требует не пустой ответ на вопрос «доказано ли заявленное»).
Возврат автору: либо добавить сцену/вариант с явным нестандартным
color/fill_color/opacity у одной из лестниц фикстуры и принять
получившийся кадр, либо сузить текст AC13 в issue до того, что матрица
реально показывает (default + оба направления + spiral + обе темы, без
пункта про custom-цвета) и явно отметить, где custom-цвет доказан иначе
(смок).
Это Medium в скоупе задачи (сам golden-фикстур лестниц — часть
demo/golden/matrix.mjs, явно перечисленного в §11 ТЗ как область
изменений и являющегося прямым deliverable AC13/§13 плана тестов). Без
блокирующего High это дает жёлтый вердикт и возврат автору, а не отдельный
issue (#202).
Что проверено и корректно
- Геометрия трапеции/направления (§5, AC4/AC5). Стрелка
(
from/tipвstairRenderGeometry) больше не зависит отdirection— вычисляется один раз по канонической локальной оси; разворачивается толькоstartHalfWidth/endHalfWidth(0.4/0.5 в зависимости отforward/backward). Совпадающее с внешним прямоугольником широкое основание не дублируется: третья линия трапеции всегда берёт узкую (0.4) сторону. Таблица режим→legacy→узкое/широкое основание из ТЗ сходится с кодом построчно. - Равномерный шаг (§6, AC6).
stairIntervalCount— чистая функция:lower=floor,upper=ceil, сравнениеerror(upper) <= error(lower)корректно выбирает большее N на равенстве (строгое<=в пользуupper). И прямая, и спиральная геометрия используют её на физической длине в см (stair.length * cellCm * GRID_N— точная инверсия старогоcmToNorm), а не на нормализованных единицах. - Кеш геометрии не зависит от стиля (AC14).
renderFingerprintстроится только изkind/x/y/angle/direction/length/width/radius(иcellCm/scale) — цветовые поля туда не входят; я убедился прогоном теста, что правкаcolor/opacity/fill_color/fill_opacityу уже закешированного объекта не создаёт новую геометрию, а поворот — создаёт. - Snapshot и legacy fallback (§4, §9, AC1/AC8).
stairVisualStyleне мутирует переданныйstair(проверено и юнитом, и построением функции — она только читает и возвращает новый объект). Материализация происходит только вsaveDialogчерезstairVisualFields(dialog), и «unchanged»-детектор специально сравнивает уже материализованныйnextсcurrent— поэтому первый Save старой записи всё равно запишет полную четвёрку, даже если пользователь не менял ни одного видимого значения. Это осознанное и корректное поведение, соответствует ТЗ дословно. - Смена типа сохраняет цвета (§9, AC2/AC9).
convertStairKindпереписан на деструктуризацию{ kind, direction, length/width/radius, ...common }—commonвключает цветовые поля и переживает конверсию в обе стороны. - Backend-контракт (§9, AC8/AC9).
STAIR_SCHEMAполучилcolor/fill_colorчерез существующий_COLORиopacity/fill_opacityчерез_finite+Range(0, 1)— точное повторение уже существующего_DECOR_COMMON-паттерна, не изобретение нового. Позитивные списки вimport_export.py/support_package.pyдополнены четырьмя полями; тесты явно проверяют, что случайноеunknown/future_stairполе всё равно отбрасывается, а разрешённые — нет. - PDF остаётся монохромным при тех же путях (§7, AC11). Новый unit-тест
#683 PDF keeps stair visual styling monochrome…— я прогнал — сравнивает командный поток PDF для стилизованной и обычной лестницы и требует их побитового равенства; отдельно проверяет, что трапеция и равные ступени реально попадают в PDF-путь (≥5 линий толщиной 0.25мм). - Cursor pointer только у active-ссылки (§7, AC12). Живой DOM-смок
(
getComputedStyle(linkedNode).cursor === 'pointer') — не только проверка класса/атрибута, а фактическое вычисленное CSS-свойство в headless Chromium; я прогнал его сам и получилtrue. Регрессионные проверки дляmissing/self/deleted/fixedсостояний (data-target-state) не задеты диффом и не сломаны. - i18n (§10, AC7). Ключи
stairs.forward/stairs.backwardзаменены (не алиасированы) наstairs.up/stairs.downво всех 4 языках одновременно; grep по всему дереву не нашёл ни одной оставшейся ссылки на старые ключи. Общий i18n-тест ключевого паритета зелёный. - Бюджет (§14).
INITIAL_VIEW_GZIP_CEILINGподнят с 299700 до 300200 внутри того же общего лимита 301066 (не расширен), с подробным обоснованием в комментарии и правкой того же коммита — не «тихая» правка. - Манифест схемы (
scripts/config-schema.json). Отдельныйfix-коммит корректно регенерирует 4 новых поля тем же детерминированным скриптом, найденная автором сама несостыковка (устаревший Validate) обработана штатно (повторный S7, не апелляция к владельцу). - Golden-принятие. Изменились ровно 4 объявленные сцены; аттестация
Baseline-Reviewed-Localвbaselines-index.json.localAttestation.sha256дословно совпадает с трейлером коммита2c9ff32d; полный прогонgolden:verify, который я выполнил сам в этой сессии, зелёный на всех 184 сценах, включая 4 сцены лестниц — регрессий нет нигде в матрице.
Чего не проверял
- Полный
python -m pytest tests_backendс реальным Home Assistant — недоступно в этом окружении (нет.venv-backend); опираюсь на CI-прогон с harness (run 36346278615) на backend-эквивалентном дереве, а не на голый pytest. - Полный browser-smoke матрицы (278 смоков) — не требуется гейтом ревью
(§8, предрелизный объём); прогнал только
smoke-select-совпадения и явно AC-именованныйdemo/smoke_stairs.mjs. performance_smoke/perf-профили — не названы в AC явно (AC14 говорит «performance-smoke/review», и я закрыл это через прямой прогонmodel-invariants+ чтение бюджета O(N); отдельный perf-профиль не прогонял, посчитал избыточным для локальной геометрии одной лестницы).- Реальный ручной клик в браузере (десктоп/тач) — ручного тестирования в
цикле нет по регламенту; полагаюсь на смок с реальным
getComputedStyleи golden-скриншоты вместо этого. - Не проверял 2.5D-специфичный рендер глазами за пределами принятого
golden-кадра
stairs-isometric-dark(доверяю авторскому visual-diff review, отмеченному в handoff).
Вердикт
Одна находка Medium (M1) внутри скоупа задачи: заявленное в AC13 доказательство («golden» показывает «пользовательские цвета») фактически отсутствует — все четыре принятые сцены лестниц используют только fallback-стиль. Остальные 13 AC полностью доказаны исполненными гейтами (частично прогнанными мной лично в этой сессии) или чтением с прямой ссылкой на строку кода. High-находок нет.
Жёлтый. Возврат автору: закрыть M1 (добавить кастомный цвет/заливку в
stairLayerFixture и принять получившийся кадр, либо явно сузить текст
AC13 под фактический охват и указать альтернативное доказательство для
оставшейся части фразы).
Материал раунда
- Ветка:
issue/683-stair-visuals, коммит20d2863eea3f— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
0c19e8c122b0627c86c740178ee3785f35e3ea1cgit log --all --format='%H %T' | grep 0c19e8c122b0 - Тело issue:
5f2e2a957f1cd2869b4e054901cbce315227e529cb9fcc68495857756083a7f6 - Вердикт конвейера:
yellow· High 0