Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности (`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` → `legacy/specs/`), а относительные ссылки внутри перенесённых документов и в соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно только до переноса документов ревью). Гейты архив не смотрят. `reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не резолвится от нового места, а цель находится от нового или старого места через карту переносов; битая и до переноса ссылка не трогается. `--apply` делает это само, `--repair-links=<rev>` — для всех переименований `<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом `--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты: перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет; мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и DEVELOPMENT › Release называют переписывание и `--check-links`. Issue: #682 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
25 KiB
Code review — issue #146, cycle r1
Вердикт: красный · цикл r1/4 · High: 1 · Medium: 0
Ветка: issue/146-four-phase-sun-background · implementation-коммит
debb13b
· ТЗ: docs/specs/146-four-phase-sun-background.md
(зелёный SPEC-REVIEW-146-r1, High:0, Medium:1 → #147,
не блокирует).
Скоуп проверки
git diff origin/dev...HEAD — 35 файлов, 2575(+)/782(-). Ядро изменений:
src/sun.ts (новый resolveDayCycle/палитра/резолвер), src/day-cycle-render.ts
(новый файл — общие layer-шаблоны), src/houseplan-card.ts (lifecycle,
defaults, рендер стейджа), src/space-card.ts/src/space-render.ts
(статическая карточка), src/styles.ts (весь визуальный контракт),
custom_components/houseplan/{store,const,import_export}.py (миграция и
export/import), src/i18n/{en,ru}.json, тесты (test/sun.test.mjs,
tests_backend/test_ha_import_export.py, smoke), документация (SUN.md,
ARCHITECTURE.md, CONFIG-COMPATIBILITY.md, USER-GUIDE.ru.md,
TESTING.md, STYLING-HOOKS.md) и оба changelog.
Единственный implementation-коммит debb13b несёт Issue: #146 ·
User-Visible: yes; оба changelog обновлены в этом же коммите — требование
выполнено. Три копии bandle идентичны (см. ниже).
Как проверялось
Дешёвые гейты (всегда):
npx tsc --noEmit→ зелёный, без вывода.npm test→ 802/802 green.npm run build→ зелёный; свежая пересборка дала SHA-25667293527e2154b2bc879d45a173dfba64c5a70cd090e770059e2b08c7c6ea0feдля всех трёх файлов (dist/houseplan-card.js,custom_components/houseplan/frontend/houseplan-card.js,demo/srv/assets/houseplan-card.js) — совпадает с хендофф-комментарием автора;git statusпосле билда пуст (закоммиченные копии побайтно совпадают со свежей сборкой).
Гейты по необходимости (diff меняет рендер/CSS-layering/lifecycle на всех поверхностях фона — обоснование запуска целевых smoke и golden):
node demo/smoke_bg_color.mjs→ все проверкиtrue,OK(paper opacity, night-does-not-dim-plan, static/kiosk/space-card parity).node demo/smoke_sun.mjs→ все проверкиtrue,OK(существующие оконные лучи/compass не задеты).node demo/smoke_sun_live_bg.mjs→ все проверкиtrue,OK.node demo/smoke_general_settings.mjs→ все проверкиtrue,OK, включаяnewSpaceUsesDaynight/floorImportUsesDaynight(AC10).node demo/smoke_render_perf.mjs→ все проверкиtrue,OK, включаяclockTickModelBuilds: 0/modelBuildsPer10Renders: 0— прямое доказательство AC14 (нет geometry/device rebuild на clock tick).npm run golden:verify(полный набор, инструмент запрещает частичный прогон в режиме verify) → нашёл найденный ниже High. Дополнительно: 4 новыеday-cycle-*сцены и другие тематические сцены (plan-snap-*,wall-junctions-*,isometric-wall-junctions-dark) —missing-baseline; для контроля я прогнал тот жеgolden:verifyна чистомorigin/dev(тот же Linux/Chromium в этой среде,demo/golden/baselines/**не менялся этим diff'ом) — тамplan-snap-*/wall-junctions-*тоже отсутствуют, аgeometry-plan-editor-dark/tray-wide-selection-en/tray-wide-tool-ru/tray-medium-group-enтожеdifferent. Это подтверждает: это все — пред-существующая среда-специфичная нестабильность/техдолг, не относящийся к #146. Ноlarge-house-zoom-250-darkнаorigin/devpassed, а на этой ветке (дважды, воспроизводимо) —different. Разбор ниже, в «Находках».python -m pytest tests_backend— не прогонялся: в этой среде отсутствует пакетhomeassistant(ModuleNotFoundError), значитtest_ha_*.py(включая новыйtest_ha_import_export.py) молча пропускаются, а зелёный результат ничего не доказывает (AGENTS.md, «Backend»). Backend-логика (store.py::migrate_config_background_mode,import_export.py) и сам новый тест проверены чтением, не исполнением — см. «Что проверено».- Performance-профили/Full Performance — не прогонялись: предрелизный гейт
(AC16), а таргетированный
smoke_render_perf.mjsуже дал прямое числовое доказательство отсутствия rebuild на clock tick (см. выше).
Находки
[High] .zoomwrap's новый z-index: 1 прячет .zoombadge за планом на любом zoom>100% — независимо от bg_mode
Файл: src/styles.ts:181-185 (.zoomwrap { position: absolute; inset: 0; z-index: 1; } — строка z-index: 1 добавлена этим diff'ом) против
src/styles.ts:472-485 (.zoombadge — position: absolute, без z-index)
и src/houseplan-card.ts:14758-14760 (${this._zoom > 1 ? html
: nothing}, рендерится как сиблинг .zoomwrap,
после него в DOM).
Причина. До этого diff'а ни .zoomwrap, ни .zoombadge не имели
z-index — оба в слое auto, порядок рисования определялся порядком в DOM, и
.zoombadge (декларирован ПОСЛЕ .zoomwrap) закономерно рисовался сверху.
Этот diff добавил .zoomwrap { z-index: 1 }, чтобы держать план выше нового
.hp-day-cycle-env { z-index: 0 } (styles.ts:201-208). Но
.hp-day-cycle-env и без этого правила уже стоял бы ниже .zoomwrap по чистому
порядку DOM (он декларирован раньше) — явный z-index: 1 был не нужен для
заявленной цели и стал побочным регрессом: положительный z-index выводит
ВСЁ содержимое .zoomwrap (план, комнаты, устройства, measure-layer) в более
высокий слой стекинга, чем любой сиблинг с z-index: auto, независимо от
порядка в DOM. .farhint/.homearrow/.recoveryoverlay/.bootveil не
задеты — у них уже был явный z-index (12/12/75/40). У .zoombadge его не
было и не появилось.
Сценарий отказа: любой пользователь View/kiosk, увеличивающий план сверх
100% (масштабирование колёсиком/пинчем — обычное действие, никак не связанное
с bg_mode), теряет видимый индикатор процента зума в левом нижнем углу —
элемент существует в DOM (pointer-events: none, не блокирует
взаимодействие), но визуально полностью скрыт под планом.
Воспроизведено, не только прочитано:
npm run golden:verifyна этой ветке — сценаlarge-house-zoom-250-dark(View,zoom: 2.5, фикстураbg_modeне задан → эффективный режимstatic, то есть день/night-фон здесь вообще не активен — регресс не специфичен дляdaynight) даётdifferent; повторный прогон — тот же результат (детерминированно, не флейк).- Тот же
golden:verifyна чистомorigin/dev(тот же чекаут, тот же Chromium) — та же сцена passed. - Визуальное сравнение
demo/golden/baselines/large-house-zoom-250-dark.png(бейзлайн) иartifacts/golden/actual/large-house-zoom-250-dark.png(текущий рендер): единственное отличие — плашка250%в левом нижнем углу присутствует на бейзлайне и полностью отсутствует на актуальном рендере;artifacts/golden/diff/large-house-zoom-250-dark.pngподсвечивает magenta ровно область этой плашки, весь остальной кадр — «без изменений» (серый с низкой альфой).
Не покрыто ни одним AC/smoke этой задачи — ни один из существующих
demo/smoke_*.mjs не проверяет видимость .zoombadge при zoom>1, поэтому
npm test/targeted smoke зелёные, а регресс виден только через golden-пиксели
существующей (не новой) сцены large-house-zoom-250-dark.
Требуется: дать .zoombadge собственный z-index выше 1 (например,
как у .farhint/.homearrow — 12), либо убрать не обязательный для цели
z-index: 1 с .zoomwrap (естественный порядок DOM уже держит его выше
.hp-day-cycle-env без этого правила) — на выбор автора; и повторный прогон
npm run golden:verify (весь набор — гейт verify не даёт частичного) до
passed на large-house-zoom-250-dark, а также визуальный просмотр 4 новых
day-cycle-* сцен перед их принятием как baseline.
Что проверено и корректно
- AC1 (real-sun резолвер границ) —
test/sun.test.mjs(«day cycle: real sun selects exact four-phase boundaries») бьёт по обеим сторонам ±6° и точным границам приrising/falling; тест умеет падать (замена<=/>=на</>вdayCyclePhaseFromSunнемедленно ломает утверждения на границах). - AC2 (atomic clock-fallback на garbage) —
test/sun.test.mjs(«strict sun snapshot keeps rays compatibility separate») перебирает missing/NaN/Infinity/non-boolean для каждого поля и подтверждаетnull(→ fallback), аsunStateOf(оконные лучи) не получил новое требованиеrising— соответствует ТЗ §7.1.smoke_sun_live_bg.mjs:invalidSampleUsesClock: true. Известное расширение до «любого из elevation/azimuth/rising» (а не только elevation/rising) реализовано именно так, как задокументировано в Medium-1 зелёного SPEC-REVIEW (→ #147, сознательно не блокирует, не новая находка). - AC3 (позиция света следует реальным данным, fallback — дуга прототипа,
geometry не пересчитывается) —
test/sun.test.mjs(«real decorative position…», «fallback arc and night visibility…») численно совпадает с §8.1/ §8.2 ТЗ (east/south/west/north, дуга 05:00→13:00→21:00);night.sunOpacity === 0подтверждён и в unit, и вsmoke_sun_live_bg.mjs(nightLightHidden: true). Отсутствие geometry rebuild — прямое числовое доказательствоsmoke_render_perf.mjs(см. «Как проверялось»). - AC5 (пиксели плана не меняются между фазами) — прочитан
src/styles.ts:198-248: единственный phase-зависимый эффект на SVG —filter: drop-shadow(...)на групповом.hp-paperg(три zero-offset drop-shadow, как требует §9) — drop-shadow не подмешивает цвет в пиксели самой фигуры, только рисует внешний halo за её alpha-контуром, поэтому внутренность footprint остаётся байт-в-байт прежней. Подтверждено визуально:artifacts/golden/actual/day-cycle-dawn-dark.pngи...night-dark.png— комнаты/стены/labels идентичны по цвету между сценами, виден только внешний halo и фон вокруг плана.smoke_bg_color.mjs:nightDoesNotDimPlan/nightPaperStaysOpaque/paperOpaque: true. Старый потребительdayPhase().planDimудалён из_targetBrightness(houseplan-card.ts:908-911, теперь безусловноreturn 1) и из_stageBg/transitionBrightness— риск №1 таблицы §19 ТЗ закрыт. - AC6 (переход 1100ms, plan не мигает, reduced-motion мгновенно) —
smoke_sun_live_bg.mjs:phaseTransitionIs1100ms: true; вstyles.tsчетыре константных.hp-day-cycle-bgслоя честно кросс-фейдят черезopacity 1100ms(не декларацияtransitionна самом градиенте) — соответствует требованию §11.1 «действительно интерполируемые CSS primitives»;@media (prefers-reduced-motion: reduce)отключает все четыре анимируемых правила (transition: none). - AC7 (одна фаза на full View/kiosk/static card, editors не задеты) —
smoke_bg_color.mjs:kioskApplies/kioskSharesDay/staticCardApplies/ staticCardSharesDawn: true;_dayCycleState()возвращаетnullприviewWeight <= 0(editors) — прочитано вhouseplan-card.ts:12654-12659. - AC8 (фон работает без north/sun.sun, оконные лучи — нет) —
smoke_sun_live_bg.mjs:backgroundIndependentOfNorth/ raysStillNeedNorth: true. i18n-хинты (gs.sun_missing,gs.north_hint) корректно переписаны на «фон работает, лучи — нет» в обеих локалях. - AC9 (однократная идемпотентная миграция) — прочитан
custom_components/houseplan/store.py:29-71:migrate_config_background_mode— чистая функция, no-op при уже валидномbg_mode, no-op на документах безconfig(layout/virtual-light stores), сохраняет неизвестные поля (copy.deepcopy+ точечная модификация); вызывается только приold_minor_version < 2(после миграцииSTORAGE_MINOR_VERSION=2не перезапускает её). Новый тестtests_backend/test_ha_import_export.py::test_background_defaults_and_store_migration_preserve_legacy_viewбьёт ровно эти случаи (idempotence черезis migrated, сохранениеfuture_*/rev, no-op для layout-подобного документа) — проверено чтением, не исполнением (см. «Как проверялось», нетhomeassistant). - AC10 (новые defaults материализуют
daynight, Edit не трогает существующий mode) —smoke_general_settings.mjs:newSpaceUsesDaynight/floorImportUsesDaynight: true.custom_components/houseplan/const.py:DEFAULT_CONFIG["settings"]["bg_mode"] == "daynight". Прочитанhouseplan-card.ts:12168(space-dialog init сохраняетnull, если явного override нет) и:12961(bgModeOf(this._settings, {})— глобальный dialog открывается с текущим ЭФФЕКТИВНЫМ режимом, не константой) — открытие+сохранение существующего space/global dialog без изменения выбора не меняет сохранённый режим. - AC11 (export/import контракт §12.4) — прочитан
custom_components/houseplan/import_export.py:_materialize_global_ background/_materialize_space_background— раздельные fallback (create_exportдля space передаёт эффективный global экспортируемого инстанса, аparse_document/build_space_mergeдля одиночного space-импорта — жёсткий"static", не режим целевой установки) — в точности различие, которое требует §12.4 («legacy per-space import не наследует target»). Новый тестtest_background_mode_is_materialized_across_export_ and_legacy_importбьёт full/space export, legacy full/space import (черезparse_document) иbuild_space_merge— проверено чтением, не исполнением. - AC12 (i18n без нового режима, pointer-inert, не двигает focus/hit
targets) —
src/i18n/{en,ru}.jsonобновлены симметрично, третий публичный токен не добавлен (селектор остаётсяstatic/daynight, подтверждено чтениемhouseplan-card.ts:14025-14027)..hp-day-cycle-env—pointer-events: none(styles.ts:206) иaria-hidden="true"(day-cycle-render.ts).npm testвключает общий тест полноты i18n-ключей (802/802 green) — контентная проверка конкретных строк выполнена чтением. - AC13 (lifecycle: 30s только в fallback+visible, catch-up, cleanup) —
smoke_sun_live_bg.mjs:clockTimerArmed/hiddenStopsClock/ visibleRestartsClock/visibilityKeepsHover: true. Прочитаны_dayCycleTick/_syncDayCycleClock/_dayCycleVisibility(houseplan-card.ts:12660-12706) и их зеркало вspace-card.ts:143-190: таймер существует только приsource === 'clock'и видимой вкладке,disconnectedCallbackчистит и таймер, и_dayCycleClockKeyв обоих файлах. - AC14 (bounded update, без geometry/device rebuild, RAF/canvas/сети/
storage) — прямое числовое доказательство
smoke_render_perf.mjs:modelBuildsPer10Renders: 0,clockTickModelBuilds: 0,clockTickKeepsModel/Devices/WallUnion/SunRays: true. Diff не добавляет ниrequestAnimationFrame, ни canvas/WebGL, ни новых сетевых/HA/storage вызовов — подтверждено чтением всего diff (git show debb13b). - AC15 (гейты, документация, три bundle, один коммит) — единственный
коммит
debb13bнесёт оба трейлера, оба changelog,SUN.md/ARCHITECTURE.md/CONFIG-COMPATIBILITY.md/USER-GUIDE.ru.md/TESTING.md/STYLING-HOOKS.mdи все три копии bundle; локальная пересборка побайтно совпала (см. «Как проверялось»). - Инвариант неизменного плана (§10 ТЗ) — ни один цветовой filter
(
brightness/contrast/saturate/sepia/hue-rotate), overlay или смена fill не применяется к дереву плана; единственный phase-зависимый filter — внешний drop-shadow на группе paper (см. AC5 выше). Старые_skyPlan/_skyElev/_skySnap/.stage.daynight/.skysnapполностью удалены, а не оставлены мёртвым кодом рядом с новым путём. - Security/network verdict (AC16, §18 ТЗ) — прочитан весь diff: ни новых
HTML/URL-приёмников, ни новых сетевых вызовов, ни новых HA service calls,
ни новых storage-полей сверх заявленной
bg_mode-семантики. Негативный вердикт подтверждается.
Чего не проверял
npm run golden:accept -- --reviewed— не выполнялся и не должен выполняться на этом этапе: 4 новыхday-cycle-*сцены (и ряд не связанных с #146 сцен) остаютсяmissing-baseline; принятие baseline — предрелизный шаг по полному Linux CI артефакту (AGENTS.md,PROCESS.md§8/13), не гейт код-ревью. Сами 4 сцены просмотрены визуально (artifacts/golden/actual/ day-cycle-{dawn,day,dusk,night}-dark.png) — план не тонирован, ночью не виден световой круг, halo не создаёт по-комнатных швов; годны как кандидат на baseline после фикса High и повторного прогона.python -m pytest tests_backend— не выполнялся (нетhomeassistantв этой среде); backend-логика и новый тест разобраны чтением (см. «Как проверялось» и AC9/AC11 выше).- Полный browser smoke-suite (127 файлов) — не прогонялся; прогнаны 5
целевых smoke, относящихся к тронутым поверхностям (
bg_color,sun,sun_live_bg,general_settings,render_perf), плюс golden как более широкая пиксельная сеть — она и нашла регресс, не покрытый ни одним из этих smoke по имени. - Performance smoke / Full Performance (эталонные бюджеты) — не
прогонялись, предрелизный гейт; косвенное доказательство отсутствия rebuild
уже получено через
smoke_render_perf.mjs. prefers-reduced-motion/forced-colorsв реальном браузере с ОС-настройкой — не проверялось интерактивно; поведение разобрано чтением CSS (@media (prefers-reduced-motion: reduce)блок вstyles.ts) и соответствует контракту §13 ТЗ.- Ручной интерактивный проход в браузере (не headless) — не выполнялся; весь визуальный вывод основан на golden/smoke харнессе и прямом просмотре сохранённых PNG.
Итог
High: 1 (описан выше, блокирует — регресс видимости .zoombadge на
zoom>100%, воспроизведён детерминированно и локализован до одной CSS-строки).
Medium: 0 (единственная содержательная находка этого масштаба — расширение
azimuth-гейта на фазу — уже заведена на этапе ревью ТЗ как
#147 и не повторяется
здесь). Low: 0 новых; ранее отмеченные в SPEC-REVIEW Low-2/Low-3 не относятся к
коду.
Вердикт красный: цикл возвращается автору на исправление стекинга
.zoombadge/.zoomwrap (см. «Требуется» в находке), с последующим зелёным
npm run golden:verify на large-house-zoom-250-dark и визуальным
подтверждением 4 новых day-cycle-* сцен перед их будущим принятием как
baseline.