29 KiB
CODE-REVIEW-648-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/648 — «Sections: вертикальный resize карточки и container-owned высота плана»
- Этап:
S7-code-review(код-ревью, PROCESS.md §2.7) - Трек: полный (
P2, см. SPEC-REVIEW-648-r1.md) - Материал:
git log --oneline origin/dev..HEADиgit diff origin/dev...HEAD; вершина ветки —b7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa, база —origin/dev=f68878cc44b818b6cfbdb3866302e6febaab2445. Три коммита:7f48e690(feat, User-Visible: yes),102b2ed7(test, User-Visible: no),b7fb7cf7(test, User-Visible: no). - Заход: r1 · блокирующих циклов израсходовано 0 из 4 (первый раунд
код-ревью; предыдущий раунд — ревью ТЗ,
docs/reviews/SPEC-REVIEW-648-r1.md, зелёный) - Роль: ревьюер кода (не автор)
Скоуп ревью
Диапазон меняет:
src/houseplan-card.ts— новое публичное реактивное свойствоlayout(HA-сигнал), геттер_containerOwnedHeight(panelHost || layout === 'grid'),getGridOptions()→{ columns: 'full', rows: 10, min_rows: 6 }, заменаpanelHostна_containerOwnedHeightв четырёх местах расчёта высоты сцены (mode-transition target,measuredCardHeaderHeight,settleSoftStageLayout, inlinestyle="height:...");src/boot-soft-layout.ts— переименование параметраpanelHost→containerOwnedHeightвmeasuredCardHeaderHeight/settleSoftStageLayout(поведение не меняется, кроме нового источникаtrue);src/styles/base.styles.ts— CSS-селекторы:host([layout="grid"])добавлены рядом с существующими:host([panel-host]);demo/smoke_sections_resize.mjs(новый, 221 строка) — browser smoke;demo/smoke_houseplan_panel.mjs— соседний regression-oracle (AC7);test/houseplan-panel.test.mjs— обновлён текстовый oracle контракта #486;scripts/mutation-registry.mjs(4 новых мутанта),scripts/smoke-links.mjs(регистрация smoke по символам);- документация:
docs/ARCHITECTURE.md,docs/CHANGELOG.md,docs/CHANGELOG.ru.md,docs/STATUS-FEATURES.md,docs/TESTING.md,docs/USER-GUIDE.md,docs/USER-GUIDE.ru.md; dist/**,custom_components/houseplan/frontend/**(класс D, пересобранный бандл — сверено побайтно, см. «Как проверялось» п.2).
Первый вопрос — какую строку docs/SCOPE.md задача обслуживает: прямого
Core-job нет, это инфраструктурная UX-правка поверхности, на которой живут
J1–J7 для персоны Home admin (Sections — основной тип dashboard в HA,
задача не расширяет функциональность плана и не входит в «Out of scope»).
Ровно так же было принято ревьюером ТЗ в SPEC-REVIEW-648-r1.md — конфликта
со SCOPE.md нет.
Как проверялось
- Прочитаны
docs/SCOPE.md,AGENTS.md,docs/process/REVIEWER.md, тело issue #648 (раздел## ТЗ, K1–K12, AC1–AC10), иdocs/reviews/SPEC-REVIEW-648-r1.md(единственный предыдущий раунд — ревью ТЗ, зелёный, без Medium/High). - Прочитан полный
git diff origin/dev...HEADпо каждому изменённому файлуsrc/**,demo/smoke_sections_resize.mjs,demo/smoke_houseplan_panel.mjs,test/houseplan-panel.test.mjs,scripts/mutation-registry.mjs,scripts/smoke-links.mjs, всех изменённыхdocs/*.md. - Пересобрал бандл (
npm run build→npm run bundle:sync) на материале —git statusпосле сборки чист: committeddist/**иcustom_components/houseplan/frontend/**побайтно совпадают с пересобранными, обновлять было нечего. - Прогнаны гейты (список — «Что прогнал» ниже); дешёвые (
typecheckвнутриbuild,npm test,npm run build) уже подтверждены зелёным Validate на этом точном SHA (см. ссылку в задании), их не переганивал;check-docs.mjs,no-new-any.mjs,no-new-private-writes.mjs,bundle:budgetпрогнал сам, так как диф трогаетsrc/**, а инструкция не считает их покрытыми чужим прогоном за меня. - Прогнал сам
smoke_sections_resize.mjsиsmoke_houseplan_panel.mjs(браузерные смоки Validate не гоняет внеheavy-условий — они не в материале, который мне назвали зелёным). Оба зелёные. - Через
node scripts/mutation-registry.mjs --id=<id>прогнал все четыре новых мутанта задачи (sections-grid-falls-back-to-viewport-height,sections-editor-transition-uses-window-height,sections-grid-drops-stage-flex-chain,sections-resize-drops-stage-refit-observer) — каждый краснит именноsmoke_sections_resize.mjs, репозиторий чист после каждого прогона (скрипт сам возвращает мутант и пересобирает). Это заполняет колонку «чем краснеет» для защитных AC2/AC4/AC5/AC7/AC8 предметно, а не по заявлению автора. - Прогнал
node scripts/smoke-select.mjs --base f68878cc4... --head b7fb7cf7e...— «прямое совпадение» вернуло 19 смоков, включая оба названных в AC (smoke_sections_resize.mjs,smoke_houseplan_panel.mjs) и ряд смоков, делящих символы_hdrH/_bootSoft/_stageEl/_warmSlotс изменённымboot-soft-layout.ts. Из них выбрал и прогналsmoke_render_invalidation.mjs(единственный, читающий_bootSoftнапрямую — сигнатураsettleSoftStageLayoutизменилась) иsmoke_warm_dialogs.mjs(_hdrH+_warmSlot, соседняя soft-layout цепочка) как точечную regression-проверку рефактора параметраpanelHost → containerOwnedHeight; оба зелёные. Остальные 15 прямых совпадений и 28 слабых связей не гонял — сочтено непропорциональным для рефактора, который для существующих layout (layoutне установлен) математически не меняет значениеcontainerOwnedHeight(былоpanelHost, осталосьpanelHost || false), см. «Чего не проверял». - Экспериментально проверил (не для отчёта CI, а для собственной
уверенности): вручную заменил
language: 'ru'на'de'в фикстуреsmoke_sections_resize.mjs, прогнал — все проверки, включаяminimumSixRowsStayContained, прошли и на немецкой локали; откатил правку (git statusпосле отката чист). Это ограничивает находку Medium-1 ниже: код сейчас не ломается на German, но CI никогда не узнает, если сломается. - Проверил трейлеры всех трёх коммитов (
git log) —Issue: #648на каждом,User-Visible: yesтолько на7f48e690, и оба changelog входят в этот же коммит (docs/CHANGELOG.md,docs/CHANGELOG.ru.md— видно вgit show --stat 7f48e690). - Сверил термины режимов (Plan/Devices/Backdrop → «План»/«Устройства»/
«Подложка») со смоком, использующим внутренние имена
plan/devices/decor— совпадает с существующим соответствием вdocs/USER-GUIDE.ru.md(уже проверено ревьюером ТЗ). - Проверил, что
houseplan-space-card(src/space-card.ts) не получил ниgetGridOptions, ни ссылок на новое свойствоlayout(grep) — юнит-тест это же утверждает черезassert.doesNotMatch. - Проверил «одно число — один источник» (§8): 10/6 фигурируют только как
возврат
getGridOptions()(единственный источник) и как проза в changelog/USER-GUIDE/ARCHITECTURE/STATUS-FEATURES/TESTING, дословно совпадающая с этим возвратом; отдельного хардкода этих чисел или пиксельных производных (≈632/≈376 из K2) вsrc/**или изменённыхdocs/*.mdне нашёл (grep).
Находки
Medium-1 (в скоупе задачи). AC3 документально обещает browser smoke
«RU/DE», а поставленный smoke проверяет минимальную высоту только на русской локали
Файл: demo/smoke_sections_resize.mjs:64-101 (секция minimumGeometry),
конфиг card.setConfig(config) строка 72.
Что не так. Таблица AC issue #648 для AC3 называет доказательство
буквально: «Browser smoke RU/DE, geometry + scroll oracle» — для
наблюдаемого результата «На минимальных 6 строках View при узкой ширине и
длинной локали не выходит за slot; header доступен...». Раздел i18n
того же ТЗ поясняет зачем: «Существующие RU/DE длинные подписи входят в
визуальную проверку минимальной высоты» — RU и DE выбраны вместе не
случайно: docs/TESTING.md:484-485 фиксирует немецкую локаль как
установленный в проекте эталон «длинных подписей», ломающих узкую/минимальную
раскладку («German View... fit at desktop and 390 px without horizontal
overflow or clipped actions»).
Фактически card.setConfig(config) в demo/smoke_sections_resize.mjs:72
задаёт language: 'ru' один раз на весь смок — включая блок
minimumGeometry (строки 91–101), который единственный проверяет минимум
6 строк на узкой ширине 390 px. Немецкая локаль нигде в файле не
упоминается (grep -n "language\|'de'" demo/smoke_sections_resize.mjs не
находит de). Половина заявленного в AC3 покрытия не существует как
исполняемый тест.
Почему это находка, а не блокер существующей защиты. Я не нашёл живого
дефекта: ручной эксперимент (заменил 'ru' на 'de' в этом же файле,
прогнал, откатил) показал, что на текущем коде minimumSixRowsStayContained
и headerContained/menuReachable проходят и на немецкой локали. Это не
регрессия сегодняшнего дня.
Сценарий отказа. Ближайшее изменение хедера/меню (например, более
длинная подпись пункта меню, добавленная в рамках другой задачи) может
сузить запас по немецкой локали на 390 px/6 строках, ничего не сломав по
русской. Ни один автотест в этом дифе и ни один существующий golden/сценарий
(проверено — docs/golden, demo/docs не содержат Sections/grid-сценария)
это не заметит: smoke_sections_resize.mjs — единственный исполняемый
свидетель AC3, и он проверяет только RU. Заявленная в согласованном ТЗ
защита «длинная локаль не переполняет минимальный slot» наполовину не
существует как код.
Почему в скоупе и не блокирует зелёный. Правка дешёвая — обернуть
существующий блок minimumGeometry (или отдельно вызвать setConfig с
language: 'de') в цикл по ['ru', 'de'] и агрегировать оба результата в
minimumSixRowsStayContained; список поверхностей и файлов не меняется, не
требует новой инфраструктуры. Без High-находок это делает вердикт жёлтым, а
не переоткрывает задачу отдельным issue (#202).
Что проверено и корректно
- AC1 (grid defaults +
getCardSize):getGridOptions()возвращает ровно{ columns: 'full', rows: 10, min_rows: 6 }безmax_rows(src/houseplan-card.ts:3762); подтверждено исполнением обоих браузерных смоков (executableGridDefaults,sectionsDefaultsRemainAvailableInPanel) и юнитом, не читающим монолит как единственное доказательство (юнит — дополнение к исполняемому свидетелю, не замена).space-card.tsне имеетgetGridOptions— подтверждено grep и юнитом. - AC2/AC3 (контейнерная высота, минимум 6 строк на 390 px): смок
smoke_sections_resize.mjsреально измеряетgetBoundingClientRect()host/card/ha-card/header/stageна живом собранном бандле — прогнан мной, зелёный (см. «Как проверялось» п.5). Кроме RU-ограничения из Medium-1, оракул содержательный: допуск 1 px,stage.height > 0,cardScroll/haCardScroll≤ 1 (нет overflow/пустого хвоста). - AC4 (серия 6→10→14→6, камера/аспект не сбрасываются, нет ошибок
ResizeObserver): проверено исполнением; мутант
sections-resize-drops-stage-refit-observer(отключение_roViewport.observe(stage)) краснит именно этот smoke — «чем краснеет» предметно подтверждено мной, не только заявлено автором. - AC5 (переходы Plan/Devices/Backdrop не меняют внешний rect, нет
busy-хвоста): смок ждёт реальный
_modeTransition.state(не форсирует приватное состояние), проверяетentering.to.stageHeight <= card.clientHeight + 1на входе и выходе для всех трёх режимов при 10 и 6 строках. Мутантsections-editor-transition-uses-window-height(возвратinnerHeightвместоcontainerOwnedHeight ? this.clientHeight) краснит smoke — подтверждено прогоном. - AC6 (explicit
grid_optionsостаётся главным, config не мутируется):Object.freeze(config)в фикстуре + сверкаJSON.stringify(config) === configBeforeиcard._config.grid_options === config.grid_optionsпо идентичности объекта — сильнее обычного deep-equal, ловит скрытое клонирование. Не отдельный юнит-файл, а часть browser smoke (ТЗ называет «Unit + browser smoke» как два способа доказательства; по факту это один исполняемый смок) — расхождение в названии способа доказательства не снижает фактическое покрытие и не поднимается до отдельной находки (по аналогии со снятыми Low ревью ТЗ). - AC7 (без
layout=grid— старое поведение;space-cardбез grid defaults):card.layout = nullв смоке восстанавливает100dvhв реальном инлайн-стиле (ordinaryCardKeepsViewportHeight), проверено исполнением;smoke_houseplan_panel.mjs(panel-host, не grid) обновлён и прогнан мной отдельно — зелёный, панельная контейнерная высота не задета рефакторомpanelHost → containerOwnedHeight(математическиcontainerOwnedHeight === panelHostкогдаlayoutне установлен). - AC8 (resize не создаёт кликов/тапов, hit-coordinates верны после
resize):
moreInfoсчётчик остаётся 0,stableMode === 'view',postResizeHitCoordinatesMatchCameraсверяет реальную матрицуgetScreenCTM()SVG после шторма resize сcard._view— не заявление, а геометрическая проверка через реальный DOM. - AC9 (30 изменений высоты без ResizeObserver loop/error): цикл из 30
frame()-тиков с чередованием 6/14 строк, финальная геометрия и время (stormElapsed < 3000) проверены исполнением;finish()вdemo/serve.mjsсчитает необработанные исключения карточки (_pageErrors) и красит прогон при их появлении — не только заявленные булевы поля. - AC10 (документация + credit):
docs/CHANGELOG.md/.ru.md,docs/USER-GUIDE.md/.ru.md,docs/ARCHITECTURE.md,docs/STATUS-FEATURES.md,docs/TESTING.mdвсе обновлены и проверены чтением;@pando80упомянут в обоих changelog с ссылкой на #648.check-docs.mjs --screenshots=warn(режим обычного пуша) прогнан мной — зелёный (единственная находка — WARN о неизменном скриншот- отпечатке, ожидаемо: диф не меняет визуальные пиксели плана/chrome, ТЗ прямо разрешает это в «Плане автотестов» п.7). - Трейлеры и changelog:
Issue: #648на всех трёх коммитах,User-Visible: yesтолько наfeat-коммите и оба changelog редактируются именно в нём — соответствует AGENTS.md. - Одно число — один источник (§8): 10 и 6 строк существуют один раз —
как возврат
getGridOptions(); вся документация и тесты лишь переиздают это значение прозой/сравнением, вторичного независимого источника не заведено. - Бандл воспроизводим: пересборка (
npm run build && npm run bundle:sync) на материале не породила диффа — закоммиченныйdist/**и зеркала вcustom_components/houseplan/frontend/**,demo/srv/assets/**побайтно совпадают с исходниками этого SHA. - Гейты, специфичные для
src/**-дифа:no-new-any.mjs(0 новыхanyв 31 добавленной строкеsrc/**),no-new-private-writes.mjs(0 новых записей в приватное состояние карточки из 223 добавленных строк смоков — только чтения, что разрешено),bundle:budget(initial View 291761 B, в пределах потолка) — все прогнаны мной и зелёные. - Юнит-тесты:
npm test— 3086 тестов, 3085 pass, 1 skip (не связан с этой задачей), 0 fail. - Область изменений:
space-card.ts,panel-host, Masonry, kiosk не тронуты кодом (только упомянуты в документации как «не меняются») — подтверждено grep по всем изменённым файлам и фактическим отсутствием правок внеhouseplan-card.ts/boot-soft-layout.ts/base.styles.ts.
Чего не проверял
- Реальное поведение Home Assistant Sections вне этого репозитория.
Весь механизм задачи опирается на внешний контракт: что настоящий HA при
размещении карточки в Sections/grid-дашборде действительно устанавливает
на элементе свойство
layout = 'grid'(K3/K11, оставлено ревью ТЗ как «принято предположительно, поменять свободно» — технический, не продуктовый вопрос). В этой песочнице нет ни реального HA, ни доступа к инструментам веб-поиска/GitHub-поиска (запрошенныеWebSearchиmcp__github__search_codeне были разрешены), поэтому свериться с исходникомhome-assistant/frontendя не смог. Все AC2–AC10 в этом дифе проверены только против синтетической фикстуры, которая устанавливаетcard.layout = 'grid'вручную (demo/smoke_sections_resize. mjs:77) — они доказывают, что если HA присылает этот сигнал, карточка ведёт себя правильно, но не доказывают, что HA действительно его присылает именно так (то же имя свойства, тот же момент жизненного цикла). Это прямой риск: если предположение неверно, весь фиче-код останется мёртвым в проде при полностью зелёных гейтах. Технически это ровно то место, которое ревью ТЗ явно делегировало код-ревью — фиксирую как открытый остаточный риск, а не как находку, потому что задача не может быть проверена исполнением без реального HA, а сам код внутри доступного материала последователен и не содержит внутренних противоречий. - 15 из 19 «прямых совпадений» и все 28 «слабых связей»
smoke- select.mjs. Прогнал толькоsmoke_sections_resize.mjs,smoke_houseplan_panel.mjs(названы в AC),smoke_render_invalidation.mjsиsmoke_warm_dialogs.mjs(точечно — единственные, трогающие изменённую сигнатуруsettleSoftStageLayout/_bootSoftнапрямую). Остальные не гонял: рефакторpanelHost → containerOwnedHeightвboot-soft-layout.tsматематически не меняет значение для существующих (не-grid) карточек, поэтому регрессия там маловероятна; полный прогон матрицы — предрелизная обязанность, а не гейт код-ревью (§8). golden:verify— не прогонял. ТЗ прямо говорит: «Golden не требуется, если визуальные пиксели плана/chrome не меняются» — диф не трогает цвета/геометрию плана, только контейнерную высоту и CSS-flex;check-docs.mjsподтверждает отсутствие изменений в зафиксированных скриншот-сценариях документации (только WARN о фингерпринте, не ERROR).npm run invariants— не прогонял: диф не меняет геометрическую модель плана (стены/комнаты/устройства) и не трогает ссылки на неё, только презентационный слой карточки.python -m pytest tests_backend— не прогонял: диф не касаетсяcustom_components/houseplan/**/*.py.- Performance-профили — не названы в AC этой задачи; ТЗ прямо
запрещает новый observer/polling, а не вводит его (новый постоянный
наблюдатель не добавлен — использован существующий
_roViewport). typecheck/npm test/npm run build/golden/performance_smoke/ browsersmoke-матрица целиком в CI — доверился зелёному Validate на точном SHAb7fb7cf7(ссылка в задании) дляtypecheck/test/build;npm testиnpm run buildтем не менее перегнал сам (см. выше) для собственной уверенности перед прогоном смоков на пересобранном бандле — оба зелёные, совпадают с Validate.
Вердикт
0 High, 1 Medium (в скоупе задачи, Medium-1 — неполное покрытие AC3
браузерным смоком: заявлены RU/DE, реализована только RU). Без High-находок
это жёлтый вердикт: автор дополняет demo/smoke_sections_resize.mjs
немецкой локалью в проверке минимальной высоты и проходит повторный раунд
код-ревью — отдельный issue не заводится (#202).
Всё остальное — AC1–AC2, AC4–AC10, трейлеры, changelog, гейты typecheck/
npm test/npm run build/no-new-any/no-new-private-writes/
bundle:budget/check-docs, четыре новых защитных мутанта и оба
относящихся к задаче browser smoke — проверено исполнением и корректно.
Жёлтый.
Вердикт: жёлтый · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 1 → в задаче
Материал раунда
- Ветка: на момент ревью без явного имени в рабочей копии; вершина —
b7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa. - База сравнения:
origin/dev=f68878cc44b818b6cfbdb3866302e6febaab2445. - Дерево материала (
git rev-parse HEAD^{tree}на момент ревью): снято на вершинеb7fb7cf7e7bce13a44ef0e1af3b97541ddb0dffa, рабочая копия чиста до и после всех локальных гейтов/мутаций (git status --porcelainпуст). - Предыдущий раунд (другой этап):
docs/reviews/SPEC-REVIEW-648-r1.md, вердиктgreen, High 0, Medium 0.
Материал раунда
- Ветка:
issue/648-sections-card-resize, коммитb7fb7cf7e7bc— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
a7fe21e21d802c04986cbfab31f3065266c8e9ffgit log --all --format='%H %T' | grep a7fe21e21d80 - Тело issue:
8400466150bde494c01aef14b7d5961be36d721b08a28c69e800bd88ac645154 - Вердикт конвейера:
yellow· High 0