Files
houseplan-card/docs/reviews/CODE-REVIEW-683-r1.md
T
2026-09-27 20:51:06 +00:00

24 KiB
Raw Blame History

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 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 0c19e8c122b0627c86c740178ee3785f35e3ea1c
    git log --all --format='%H %T' | grep 0c19e8c122b0
    
  • Тело issue: 5f2e2a957f1cd2869b4e054901cbce315227e529cb9fcc68495857756083a7f6
  • Вердикт конвейера: yellow · High 0