Files
houseplan-card/docs/reviews/CODE-REVIEW-648-r3.md
T
2026-09-25 13:33:26 +00:00

353 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-648-r3
- **Issue:** https://github.com/Matysh/houseplan-card/issues/648 —
«Sections: вертикальный resize карточки и container-owned высота плана»
- **Этап:** `S7-code-review` (код-ревью, PROCESS.md §2.7)
- **Трек:** полный (`P2`, см. `docs/reviews/SPEC-REVIEW-648-r1.md`)
- **Материал:** `git log --oneline origin/dev..HEAD` и
`git diff origin/dev...HEAD`; вершина ветки — ровно
`4168b1cfcedcc7979e17a380427d4466b45f6b2b`, база — `origin/dev` =
`7372e977b89a0a907b67cf00c1b2cd2b9ed2ddfa`. Семь коммитов (в порядке
предок→потомок): `230813b3` (docs, SPEC-REVIEW-648-r1), `2452c51d` (feat,
User-Visible: yes), `dc0a5ccc` (test), `b9fe9166` (test), `180239f4` (docs,
CODE-REVIEW-648-r1), `4168b1cf` (docs, индекс), `8f13325c` (test).
- **Заход:** r3 · блокирующих циклов израсходовано 2 из 4 (по заданию
оркестратора)
- **Роль:** ревьюер кода (не автор)
## Аномалия материала — зафиксировано, не блокирует
Прежде чем перейти к разбору: я не смог найти в репозитории и в issue #648
никакого артефакта раунда r2 код-ревью.
- `docs/reviews/` содержит только `CODE-REVIEW-648-r1.md` (git-история
подтверждает — `git log --all -- 'docs/reviews/CODE-REVIEW-648*'` называет
единственный коммит `180239f4`).
- В issue 11 комментариев; последний — от автора в 13:07:27 UTC
(«Medium-1 ревью r1 исправлен... Жду зелёного push Validate и затем заново
запускаю S7»). Комментария с вердиктом r2 нет.
- Таймлайн меток issue показывает `S7-code-review` → `S6-in-progress` в
13:04:13 (это возврат по жёлтому r1), затем `S6-in-progress` →
`S7-code-review` в 13:11:02 (после коммита с RU/DE-фиксом), и ещё одну
перестановку той же метки в 13:15:30→13:15:32 без промежуточного
комментария. Ни один из этих переходов не сопровождён вторым
жёлтым/красным комментарием ревьюера.
- SHA, которые называл r1 (`b7fb7cf7…`, `76a1d5c0…` и т. д.), в текущей
истории не существуют (`git cat-file -t` — «could not get object info»):
ветка была перебазирована на ушедший вперёд `dev` (текущий `origin/dev` —
`7372e977`, слияние другой задачи #649; старая база r1 `f68878cc` —
предок текущего `dev`, не наоборот). Это подтверждённый ребейз на
продвинувшийся `dev`.
Согласно условию задания «дельта не локальна, если… ребейз на ушедший вперёд
`dev`» — это ровно тот случай, поэтому ниже **полный** разбор, а не
дифф-от-r2. Как единственный найденный предыдущий раунд использую
`CODE-REVIEW-648-r1.md`; расхождение номера (r1 найден, а не r2) — вопрос
целостности конвейера/бюджета, а не кода этой задачи, и не влияет на
техническую строгость проверки ниже: полный прогон гейтов и AC перекрывает
любые находки, которые мог бы содержать пропавший r2.
## Скоуп ревью
Диапазон правит:
- `src/houseplan-card.ts` — публичное реактивное свойство `layout`
(HA-сигнал), геттер `_containerOwnedHeight` (`panelHost || layout ===
'grid'`), `getGridOptions()` → `{ columns: 'full', rows: 10, min_rows: 6 }`,
использование `_containerOwnedHeight` вместо `panelHost` в расчёте высоты
mode-transition, `measuredCardHeaderHeight`, `settleSoftStageLayout` и
inline `style="height:…"`;
- `src/boot-soft-layout.ts` — переименование параметра `panelHost` →
`containerOwnedHeight` в тех же двух функциях (поведение не меняется,
кроме нового источника `true`);
- `src/styles/base.styles.ts` — `:host([layout="grid"])` добавлен рядом с
`:host([panel-host])` во всех соответствующих селекторах;
- `demo/smoke_sections_resize.mjs` (новый, 225 строк) — 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/**`, `demo/srv/assets/**`
(класс D, пересобранный бандл — сверено побайтно, «Как проверялось» п.3);
- `docs/reviews/CODE-REVIEW-648-r1.md`, `docs/reviews/SPEC-REVIEW-648-r1.md`,
`docs/reviews/INDEX.md` — материал предыдущих раундов, не код продукта.
Первый вопрос — какую строку `docs/SCOPE.md` задача обслуживает: прямого
Core-job нет, это инфраструктурная UX-правка поверхности, на которой живут
J1–J7 для персоны Home admin (Sections — основной тип dashboard в HA; задача
не расширяет функциональность плана и не входит в «Out of scope»). Так же
было принято ревьюером ТЗ в SPEC-REVIEW-648-r1.md — конфликта со SCOPE.md
нет; за прошедшие раунды это не изменилось.
## Как проверялось
1. Прочитаны `docs/SCOPE.md`, `AGENTS.md`, `docs/process/REVIEWER.md`
(в материале отсутствует — читал разделы PROCESS.md §2.4, §2.7, §2.10,
§4, §7.2, §8, §12 напрямую), тело issue #648 (`gh issue view 648
--json body`, разделы ТЗ, K1–K12, AC1–AC10) и все 11 комментариев
issue (`gh issue view 648 --json comments`), таймлайн меток
(`gh api repos/Matysh/houseplan-card/issues/648/timeline`).
2. Прочитан полный `git diff origin/dev...HEAD` по каждому изменённому файлу
`src/**`, обоим browser smoke, юнит-тесту, `mutation-registry.mjs`,
`smoke-links.mjs`, всем изменённым `docs/*.md`.
3. Пересобрал бандл (`npm run build`, затем `npm run bundle:sync`) на
материале — `git status --porcelain` до и после пуст: `dist/**`,
`custom_components/houseplan/frontend/**` и `demo/srv/assets/**`
побайтно совпадают с пересобранными.
4. Дешёвые гейты (`typecheck` внутри `build`, `npm test`, `npm run build`)
уже подтверждены зелёным Validate на этом точном SHA (ссылка в задании);
тем не менее перегнал `npm run build` (п.3) и полный `npm test` сам для
собственной уверенности перед прогоном смоков на пересобранном бандле —
**3086 тестов, 3085 pass, 1 skip, 0 fail** — совпадает с отчётом Validate
и с r1.
5. Прогнал сам, так как диф трогает `src/**`: `node scripts/no-new-any.mjs`
(0 новых `any` в 31 добавленной строке), `node
scripts/no-new-private-writes.mjs` (0 новых записей в приватное состояние
из 227 добавленных строк smoke), `npm run bundle:budget` (initial View
291761 B — совпадает с r1, в пределах потолка), `node
scripts/check-docs.mjs --screenshots=warn` (WARN про фингерпринт
скриншотов — ожидаемо, диф не меняет пиксели плана), `node
scripts/validate-commit-provenance.mjs --range=origin/dev..HEAD`
(exit 0) — все зелёные.
6. Прогнал `node scripts/smoke-select.mjs --base origin/dev --head HEAD` —
«прямое совпадение» вернуло те же 19 смоков, что и в r1 (диф с r1 не
меняет затронутые символы `src/**`), включая оба названных в AC
(`smoke_sections_resize.mjs`, `smoke_houseplan_panel.mjs`). Прогнал их
плюс `smoke_render_invalidation.mjs` и `smoke_warm_dialogs.mjs` (те же
точечные регрессионные свидетели рефактора `panelHost →
containerOwnedHeight`, что r1 выбрал по символам `_bootSoft`/`_hdrH`/
`_warmSlot`) — все четыре зелёные (полный вывод в п.7 ниже и в разделе
«Что проверено»).
7. Через `node scripts/mutation-gate.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` — каждый вывел «поймано 1 из
1» (`smoke_sections_resize.mjs` краснеет предметно на каждом), `git
status --porcelain` пуст после каждого прогона (скрипт сам возвращает
патч и пересобирает).
8. Целевая проверка закрытия Medium-1 из r1: прочитал полный diff коммита
`8f13325c` — `minimumGeometry` теперь `for (const language of ['ru',
'de'])`, каждая итерация делает реальный `card.setConfig` с этим
`language` и меряет геометрию; финальный булев
`minimumSixRowsStayContainedInRuAndDe` требует `.every(...)` по обеим
записям, а не только по одной. Прогнал сам `smoke_sections_resize.mjs`
целиком — `minimumSixRowsStayContainedInRuAndDe: true` в выводе.
9. Проверил трейлеры всех коммитов (`git show -s --format='%H%n%s%n%b'`) —
`Issue: #648` на каждом из шести некод-ревью-документных коммитов;
`User-Visible: yes` только на `2452c51d` (feat), и `docs/CHANGELOG.md` +
`docs/CHANGELOG.ru.md` входят именно в этот коммит (проверено `git show
--stat 2452c51d`).
10. Проверил «одно число — один источник» (§8): `grep -n "rows: 10\|
min_rows\|632\s*px\|376\s*px"` по `src/houseplan-card.ts` и всем
изменённым `docs/*.md` — единственное определение чисел 10/6 —
возврат `getGridOptions()`; документация переиздаёт их прозой без
отдельного хардкода.
11. Проверил `space-card.ts` (grep `getGridOptions|layout`) — не получил ни
нового реактивного свойства, ни grid-defaults; юнит-тест
`test/houseplan-panel.test.mjs` подтверждает тем же
`assert.doesNotMatch`.
12. Прочитал комментарий `@pando80` от 13:06:46 UTC: внешний автор
подтвердил на реальном HA-дашборде Sections (`grid_options: rows: 6,
columns: full`, HA/интеграция 1.78.0-beta.2, включая полный restart),
что план вписывается в карточку без переполнения. Это не гейт этого
ревью и не заменяет автотест, но существенно снижает остаточный риск
r1 «реальное поведение HA Sections не проверено» (см. «Чего не
проверял»).
13. Инварианты модели (`npm run invariants`) и `pytest tests_backend` не
прогонял — диф не меняет геометрическую модель (стены/комнаты/
устройства) и не касается `custom_components/**/*.py`; обоснование
подробнее в «Чего не проверял».
## Закрытие раунда r1 (единственный найденный предыдущий раунд)
| Находка r1 | Чем закрыта | Где видно |
|---|---|---|
| Medium-1: AC3 обещает browser smoke RU/DE для минимальной высоты, но `demo/smoke_sections_resize.mjs` проверял только `language: 'ru'` | Коммит `8f13325c` переписал блок `minimumGeometry` в цикл `for (const language of ['ru', 'de'])` с реальным `card.setConfig({..., language})` на каждой итерации; итоговый флаг `minimumSixRowsStayContainedInRuAndDe` требует `.every(...)` по обеим записям | `demo/smoke_sections_resize.mjs:91-104,199-201`; лично прогнан — `minimumSixRowsStayContainedInRuAndDe: true` в выводе (см. «Как проверялось» п.8) |
## Унаследовано из r1 (не перепроверялось заново, а подтверждено полным прогоном)
Поскольку разбор в этом раунде полный (ребейз на ушедший вперёд `dev`), ниже
не «унаследовано без проверки», а формально повторно подтверждено —
перечисляю соответствие r1, чтобы не дублировать формулировки находок:
- AC1, AC2, AC4–AC10, четыре защитных мутанта, оба relevant browser smoke,
трейлеры/changelog, `typecheck`/`npm test`/`npm run build`/`no-new-any`/
`no-new-private-writes`/`bundle:budget`/`check-docs` — то же самое
доказательство, что в `CODE-REVIEW-648-r1.md`, лично перегнано заново в
этом раунде на текущем SHA (см. «Как проверялось» пп. 4–7, 9–11) и
совпадает с описанием r1 буквально (тот же код: `2452c51d` содержит
идентичный итог трёх исходных коммитов, которые ревьюировал r1, просто
под новыми SHA после ребейза).
- Остаточный риск «реальное поведение HA Sections вне репозитория не
проверено» — не закрыт кодом (репозиторий по-прежнему не может это
проверить исполнением), но частично снижен независимым внешним
подтверждением `@pando80` на реальном HA (см. «Как проверялось» п.12) —
остаётся в «Чего не проверял» с этим уточнением.
## Находки
Ни одной новой находки. Единственная находка r1 (Medium-1) закрыта
предметно и проверена исполнением (см. таблицу выше).
## Что проверено и корректно
- **AC1** — `getGridOptions()` возвращает ровно `{ columns: 'full', rows:
10, min_rows: 6 }` без `max_rows` (`src/houseplan-card.ts:3762`);
`executableGridDefaults: true`; `getCardSize() === 12` не изменён
(`src/houseplan-card.ts:3758`); `space-card.ts` без `getGridOptions` —
подтверждено grep и юнитом.
- **AC2** — `defaultTenRowsFillTheSlot: true`;
`gridUsesContainerOwnedInlineHeight: true`;
`gridSignalReflectsForScopedCss: true` — реальный `getBoundingClientRect`
на живом бандле, допуск 1 px, `cardScroll`/`haCardScroll` ≤ 1.
- **AC3** — `minimumSixRowsStayContainedInRuAndDe: true` — теперь по обеим
локалям (см. закрытие Medium-1 выше), header не обрезан, меню достижимо
в обеих.
- **AC4** — серия 6→10→14→6:
`resizeChangesStageAtEveryRowCount: true`; мутант
`sections-resize-drops-stage-refit-observer` краснит smoke — прогнан
лично.
- **AC5** — `allEditorsStayInsideTheFixedSlot: true`,
`editorTransitionFramesUseTheFixedSlot: true`,
`editorTransitionsLeaveNoBusyTail: true` — реальный
`card._modeTransition.state`, не форсированное приватное состояние;
мутант `sections-editor-transition-uses-window-height` краснит smoke —
прогнан лично.
- **AC6** — `explicitGridOptionsStayExternal: true` — `Object.freeze(config)`
+ сверка `JSON.stringify(config) === configBefore` и
`card._config.grid_options === config.grid_options` по идентичности
объекта.
- **AC7** — `ordinaryCardKeepsViewportHeight: true` (реальный inline-style
`100dvh` при `card.layout = null`); `smoke_houseplan_panel.mjs` →
`sectionsDefaultsRemainAvailableInPanel: true` (панель не задета
рефактором `panelHost → containerOwnedHeight`, поскольку
`_containerOwnedHeight === panelHost`, когда `layout` не установлен) —
прогнан лично, зелёный.
- **AC8** — `resizePreservesCameraAndIntent: true` (0 `hass-more-info`,
`_space`/`_mode` не смещаются);
`postResizeHitCoordinatesMatchCamera: true` — сверка реальной
`getScreenCTM()` после шторма resize.
- **AC9** — `resizeStormSettlesWithoutLoop: true` — 30 переключений 6/14
строк, `stormElapsed < 3000`; `finish()` красит прогон при необработанных
исключениях карточки (`_pageErrors`), не только по заявленным булевым
полям.
- **AC10** — все шесть документов обновлены и прочитаны целиком; числа 10/6
и пиксельные ссылки (632/376) существуют только как проза, единственный
источник — `getGridOptions()` (см. «Как проверялось» п.10); `@pando80`
кредитован в обоих changelog со ссылкой на #648;
`check-docs.mjs --screenshots=warn` — WARN (не ERROR), не относится к
этому дифу.
- **Трейлеры и changelog**: `Issue: #648` на всех некод-ревью-документных
коммитах; `User-Visible: yes` только на `2452c51d`, оба changelog — в том
же коммите.
- **Бандл воспроизводим**: `npm run build && npm run bundle:sync` на
материале не породил диффа — все три копии (`dist/**`,
`custom_components/houseplan/frontend/**`, `demo/srv/assets/**`)
побайтно совпадают с исходниками этого SHA.
- **Гейты для `src/**`-дифа**: `no-new-any`, `no-new-private-writes`,
`bundle:budget`, `validate-commit-provenance` — все зелёные (см. «Как
проверялось» пп. 5, 9).
- **Юнит-тесты**: `npm test` — 3086, 3085 pass, 1 skip (не относится к
задаче), 0 fail.
- **Область изменений**: `panel-host`, Masonry, kiosk, `space-card.ts` не
тронуты кодом — подтверждено grep и фактическим составом дифа.
## Чего не проверял
- **Реальное поведение Home Assistant Sections вне этого репозитория.**
Как и в r1 — нет ни реального HA, ни доступа к `WebSearch`/поиску по коду
GitHub в этой среде (инструменты `mcp__github__get_issue`/
`get_issue_comments` тоже были недоступны без явного разрешения; тело и
комментарии issue получены через `gh` CLI, который разрешён). AC2–AC10 в
этом дифе по-прежнему проверены только против синтетической фикстуры,
устанавливающей `card.layout = 'grid'` вручную. Новое по сравнению с r1:
комментарий `@pando80` (см. «Как проверялось» п.12) — независимое
подтверждение на реальном HA 1.78.0-beta.2 с `grid_options: rows: 6`, план
не переполняет карточку. Это анекдотическое, не воспроизводимое
автоматически свидетельство, но оно прямо адресует технический риск,
который ревью ТЗ делегировало код-ревью, и снижает его до низкого:
внешний пользователь уже наблюдал ровно тот сигнал (`layout`), на который
рассчитывает код, на реальной установке.
- **15 из 19 «прямых совпадений» и все 28 «слабых связей»
`smoke-select.mjs`.** Тот же список, что в r1 (диф с r1 не расширил
затронутые символы `src/**`); прогнал только 4 релевантных
(`smoke_sections_resize.mjs`, `smoke_houseplan_panel.mjs`,
`smoke_render_invalidation.mjs`, `smoke_warm_dialogs.mjs`) — обоснование
то же: рефактор `panelHost → containerOwnedHeight` математически не
меняет значение для существующих (не-`grid`) карточек; полная матрица —
предрелизная обязанность (§8), не гейт код-ревью.
- **`golden:verify`** — не прогонял: диф не меняет визуальные пиксели
плана/chrome, только контейнерную высоту и CSS-flex;
`check-docs.mjs` подтверждает отсутствие изменений в зафиксированных
скриншот-сценариях документации (только WARN о фингерпринте).
- **`npm run invariants`** — не прогонял: диф не меняет геометрическую
модель плана и не трогает ссылки на неё, только презентационный слой
карточки.
- **`python -m pytest tests_backend`** — не прогонял: диф не касается
`custom_components/**/*.py`.
- **Performance-профили** — не названы в AC; новый постоянный
observer/polling не введён (используется существующий `_roViewport`).
- Полная матрица browser smoke в CI (271 смок) — не прогонял целиком,
только выбранные по диффу и AC (см. выше); это соразмерно объёму гейтов
код-ревью (§8), не предрелизному гейту.
## Вердикт
0 High, 0 Medium. Единственная находка предыдущего раунда (Medium-1: неполное
покрытие AC3 браузерным смоком RU/DE) закрыта предметно — исполняемый смок
теперь проверяет минимальную высоту на обеих локалях и был лично прогнан
зелёным. Новых находок при полном разборе (обязателен из-за ребейза на
ушедший вперёд `dev`) не выявлено: все AC1–AC10, четыре защитных мутанта,
оба относящихся к задаче browser smoke плюс два точечных regression-смока,
трейлеры, оба changelog и гейты, специфичные для `src/**`-диффа
(`no-new-any`, `no-new-private-writes`, `bundle:budget`,
`validate-commit-provenance`, `check-docs`), — лично перегнаны на этом SHA и
корректны.
Материал ревью содержит аномалию бухгалтерии раундов (не найден артефакт r2 —
см. раздел выше); это вопрос целостности конвейера, а не кода задачи, и не
меняет техническую оценку: код на SHA `4168b1cf` готов.
**Зелёный.**
---
Вердикт: зелёный · заход r3 · блокирующих циклов 2/4 · High: 0 · Medium: 0
---
<!-- material-anchors: подготовлено ревьюером -->
## Материал раунда
- Ветка: без явного имени в рабочей копии на момент ревью (детектирована
как `HEAD` detached); вершина — `4168b1cfcedcc7979e17a380427d4466b45f6b2b`.
- База сравнения: `origin/dev` = `7372e977b89a0a907b67cf00c1b2cd2b9ed2ddfa`.
- Дерево материала (`git rev-parse HEAD^{tree}` на момент ревью): снято на
вершине `4168b1cfcedcc7979e17a380427d4466b45f6b2b`, рабочая копия чиста до
и после всех локальных гейтов/мутаций/пересборки бандла (`git status
--porcelain` пуст).
- Предыдущий раунд (найден): `docs/reviews/CODE-REVIEW-648-r1.md`, вердикт
`yellow`, High 0, Medium 1 (закрыт — см. таблицу выше). Раунд r2 код-ревью
не найден ни в `docs/reviews/`, ни в комментариях issue #648, ни в её
таймлайне меток — см. раздел «Аномалия материала» выше.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/648-sections-card-resize`, коммит `4168b1cfcedc` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `06653e5b743ecdf436899320c19a411f04af6286`
```
git log --all --format='%H %T' | grep 06653e5b743e
```
- Тело issue: `8400466150bde494c01aef14b7d5961be36d721b08a28c69e800bd88ac645154`
- Вердикт конвейера: `green` · High 0