Files
houseplan-card/docs/reviews/CODE-REVIEW-213-r1.md
T
2026-08-20 08:16:28 +00:00

226 lines
21 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-213-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/213
- **Спецификация:** `docs/specs/213-device-marker-geometry.md` (зелёный
SPEC-REVIEW-213-r1, High 0 · Medium 0)
- **Диапазон:** `origin/dev...HEAD` = `0c93be9` (specify) · `c749d68` (spec
review doc) · `0e5ee03` (`fix: polish device marker geometry and input`,
`Issue: #213`, `User-Visible: yes`)
- **Ветка:** `issue/213-device-marker-geometry`
- **Роль:** ревьюер кода, свежая сессия, без контекста написания ТЗ/кода.
## Скоуп проверки
Одна реализационная задача, закрывающая AC1–AC13 из спецификации: удаление
позднего `--device-visual-factor: 0.9` в пользу единого effective-base на
границе поверхности; прямой `0.55×core` MDI viewport; общая concentric-геометрия
core/shell через новый `.device-shell-frame`; opening Lock/Unlock на визуальном
пакете #179; hover/action всей видимой value-капсулы; возврат непрерывной
`lqiColor()` для marker LQI. Изменённые модули: `src/styles.ts`,
`src/device-face.ts`, `src/device-presentation.ts`, `src/houseplan-card.ts`,
`src/space-render.ts`, `src/hp-device-preview.ts`, новый
`src/device-marker-geometry.ts`; targeted smokes, unit-тесты, mutation-gate
guard, docs (`ARCHITECTURE.md`, `TESTING.md`, `USER-GUIDE.md`/`.ru.md`, оба
changelog), reference screenshots.
## Как проверялось
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | зелёный |
| Юнит | `npm test` | 955/955 зелёных, 0 skipped (расходится с хендофф-комментарием «954 passed, 1 skipped» — вероятно другое окружение автора; актуальный прогон здесь зелёный и это авторитетно) |
| Сборка | `npm run build` | зелёная; `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` и `cmp … demo/srv/assets/houseplan-card.js` — обе байт-в-байт идентичны, `git status` после пересборки чист (закоммиченный бандл свежий) |
| Документация | `node scripts/check-docs.mjs --external` | «Documentation checks passed (7 files, 10 external links)» |
| Mutation-proof (AC1, единственный названный в §18.2 guard) | `node scripts/mutation-gate.mjs --id=device-visual-factor-removed` | «тест покраснел, как обязан» — guard доказанно умеет падать |
| Таргетированные browser smoke (названы в §18.2 / затронуты диффом) | `node demo/smoke_device_icon_design.mjs` | зелёный, включая новые `valueCapsuleOwnsHoverAndActionAtEveryPosition`, `lqiUsesContinuousComputedColor` |
| | `node demo/smoke_device_icon_pixel_alignment.mjs` (новый, ядро AC3) | зелёный на всех 4 DPR; собственный mutant (`translateX(1px)`) детектируется — тест доказанно умеет падать |
| | `node demo/smoke_device_preview_parity.mjs` | зелёный, включая переписанный `resolvedBaseContract` |
| | `node demo/smoke_static_icon.mjs`, `smoke_icon_scale.mjs`, `smoke_state_value.mjs` | зелёные |
| | `node demo/smoke_opening_binding.mjs` | зелёный, включая новый `openingLockUsesPackageGeometryAndStates` (Light/Dark × locked/unlocked/unknown) |
| | `node demo/smoke_registryless_opening.mjs`, `smoke_lock_action.mjs`, `smoke_lock_invariant.mjs` | зелёные — secure no-toggle-on-plan инвариант не задет |
| | `node demo/smoke_touch_tips.mjs` | зелёный — hover/touch-контракт не задет |
| Regression-смоки, задетые переименованием `.device-shell` → `.device-shell-frame` | `node demo/smoke_hidden_flag.mjs`, `node demo/smoke_vacuum.mjs` | зелёные |
| Golden (диагностически, этот раннер — Linux) | `npm run golden:verify` | 30 из ~90 сценариев `different` — все просмотрены (`artifacts/golden/diff/*.png`): расхождение локализовано строго на маркерах устройств/lock-бейджах (обновлённая геометрия/глиф/LQI-цвет), стены/комнаты/tray/диалоги без затронутых markers не изменились. Ожидаемо по §18.3/§20 ТЗ: baseline принимается только `npm run golden:accept -- --reviewed` на полном Linux CI артефакте, это пре-релизный шаг, не гейт код-ревью |
Полный `smoke` (154 файла), `performance_smoke` и `pytest tests_backend`
сознательно не прогонялись — см. «Чего не проверял».
## AC-трассировка
- **AC1 (size contract).** `--device-visual-factor` удалён из `src/styles.ts`
(`.dev`, `.vacpuck`); `effectiveDeviceBaseSize()` в новом
`src/device-marker-geometry.ts` пересчитывает legacy `icon_size` в effective
base один раз на границе (`houseplan-card.ts`, `space-render.ts`,
`hp-device-preview.ts` — статический `48.6px`). Арифметика проверена: `(x/2.5)
× 2.25 = x × 0.9`, т.е. численно тождественно старому позднему множителю для
default/explicit/kiosk/per-marker/preview/vacuum. Юнит
`device-marker-geometry.test.mjs` покрывает границы (`NaN`→default,
пропорциональность). Mutation-proof `device-visual-factor-removed`
красный на восстановленном позднем `* 0.9` внутри `--dev-size`. **Доказано**
unit + mutation-proof + smoke `geometryMatchesAt32_56_96`.
- **AC2 (glyph).** `.dev ha-icon { --mdc-icon-size: calc(var(--dev-size) *
0.55) }` — прямой base, без второго множителя/transform; core/shell/anchor
не читают этот токен. `demo/smoke_device_icon_design.mjs`
(`geometryMatchesAt32_56_96` проверяет `iconViewport/core ≈ 0.55` и painted
bbox `≈0.458`) зелёный на 32/56/96, custom/rotated/state-swapped глиф не
проверялся отдельным smoke сверх существующего reference-матрикса, но
источник ratio один общий CSS-токен — клиппинг архитектурно маловероятен.
**Доказано** source + smoke, custom/rotated путь — чтением.
- **AC3 (concentric geometry).** Ключевая переработка: `.device-shell` больше
не несёт border/padding (`padding: 0`, `border: 0`), это чистый flex-контейнер
размера `--dev-size` (без values) или content-based (с values). Новый
`.device-shell-frame` — единственный визуальный слой, `inset: calc(var(
--device-shell-inset) / -1)` растягивает его наружу от границ `.device-shell`
на константу `--dev-size × 0.134375` (алгебраически равно старой
`(--device-shell-size − --dev-size) / 2`, то есть тот же целевой внешний
диаметр, но без независимого round у padding/border). Для `:not(.with-values)`
`.device-shell` выставлен `left:0;top:0` — совпадает с `.dev`'s собственным
боксом (который уже центрирован на сохранённой точке через
`margin: calc(--dev-size/-2) 0 0 calc(--dev-size/-2)`), поэтому core и frame
разделяют одну систему координат вместо двух независимо округляемых. Для
`with-values.pos-*` анкер аналогично зафиксирован на `0` (не
`50% − size/2`), расширение идёт только в выбранную сторону. Никаких
`translate(1px)`, UA/DPR sniffing или per-size исключений. **Доказано**
чтением (вывод формулы выше) + новый `smoke_device_icon_pixel_alignment.mjs`
(dense quarter-px DOM-центры на 24…112px, alpha-weighted painted-centroid на
12 дробных размерах × 4 DPR, red/green solid-diagnostic слои, calibrated
предел `DPR/2 + 0.1` физ. пикселя, отдельный mutant `translateX(1px)`
детектируется) + reviewed golden diff (только маркеры).
- **AC4 (anchor/parity).** `.dev` width/height/margin формула не изменилась
(тот же `--dev-size`, просто из другого источника); `44×44` пол сохранён
через `.dev::before { width/height: max(44px, --device-shell-size) }`
(не тронут). `smoke_device_preview_parity`, `smoke_static_icon`,
`smoke_icon_scale`, `hitAreaAtLeast44` в `smoke_device_icon_design` зелёные.
**Доказано** unit facts + smoke.
- **AC5/AC6 (opening locks + security).** `.oplock` разложен на
`.oplock-shell`/`.oplock-core` с ratio `1.26875`/`0.55`, Light/Dark токены и
цвета locked (`#000`/`#252525`, белый глиф) / unlocked (`#F0A00C`,
тема-зависимый глиф) / unknown (нейтральный, без ложного locked/unlocked)
сверены с §10 ТЗ и подтверждены `openingLockUsesPackageGeometryAndStates` в
`smoke_opening_binding.mjs` (обе темы × три состояния, точные RGB). Клик по
`.oplock` по-прежнему только `this._openingInfo = o` (открывает карточку),
без вызова сервиса; `smoke_lock_action`/`smoke_lock_invariant` зелёные без
изменений в их коде — инвариант `docs/SCOPE.md` (единственная санкционированная
actuation-поверхность — кнопка карточки) не затронут. **Доказано** smoke +
чтением обработчика клика.
- **AC7/AC8 (capsule hover/action).** `.device-shell-frame` получил
`pointer-events: auto` (был `none` на прежнем `.device-shell`); `.device-core`
и `.device-sections` — `pointer-events: none`. Единственный обработчик клика
остаётся на `.dev` (`@click=${(e) => this._clickDevice(e, d)}`) — событие
бабблится от любого потомка независимо от того, что визуально «капсула»
выходит за CSS-бокс `.dev` (overflow visible); `_clickDevice` не читает
`e.target`, поэтому не может задвоить действие. `:hover` на `.dev` уже был
gated через `data-pointer-hover` (`src/styles.ts:2180`) и распространяется от
любого наведённого потомка — новых hover-правил не потребовалось, только
снятие `pointer-events: none` с ранее «мёртвых» зон (gap, padding) через
frame. `smoke_device_icon_design.mjs`
(`valueCapsuleOwnsHoverAndActionAtEveryPosition`) кликает и наводит мышь на
четыре позиции value-бейджа, проверяет `elementFromPoint` попадает в
`device-shell-frame`, hover совпадает с core-hover цветом и действие
засчитывается ровно один раз на позицию (`capsuleActions === 5`, включая клик
по core). **Доказано** smoke + чтением (нет stopPropagation на пути,
единственный listener).
- **AC9/AC10 (continuous marker LQI).** `markerLqiColor()` теперь однострочно
делегирует в общий `lqiColor()` (`src/logic.ts`, формула не менялась:
`hue = clamp((lqi-40)/140×120, 0, 120)`); `markerLqiBand()` не тронут и
остаётся источником aria/data band. Юнит
`device-presentation.test.mjs` сравнивает `markerLqiColor` с `lqiColor` на
representative/boundary значениях и явно проверяет `41 !== 42` (нет ступени).
Room fill (`logic.ts:1428`, `mode==='lqi'`) и tooltip (`houseplan-card.ts:16185`)
и раньше читали `lqiColor()` напрямую, не через `markerLqiColor` — путь не
затронут. **Доказано** unit + smoke (`lqiUsesContinuousComputedColor`
сравнивает вычисленный computed-color с независимо построенным
`hsl(...)`-пробником) + чтением вызывающих мест room-fill/tooltip.
- **AC11 (data/i18n compatibility).** Diff не касается `src/i18n/*.json`,
card-config schema, миграций. Проверено чтением diff-статистики: изменённые
файлы — только `.ts`/CSS-in-JS/тесты/доки; ни один i18n JSON не в списке.
**Доказано чтением, не исполнением.**
- **AC12 (performance).** Чтением: реализация CSS-only + один чистый расчёт
(`effectiveDeviceBaseSize`), новых `ResizeObserver`/`matchMedia`/per-frame JS
нет; `backdrop-filter: none` явно сохранён и на `.oplock-shell`, и на
`.device-shell-frame`; DOM-прирост на маркер — один статический `<span
class="device-shell-frame">`, на opening-lock — два статических `<span>`, без
анимационных слоёв сверх существующих. Performance-smoke не прогонялся —
это осознанно пре-релизный гейт (§18.3 ТЗ, AGENTS.md §8), не названный в AC12
способом доказательства сверх source review. **Доказано чтением, не
исполнением**, инструментальная часть отложена по процессу.
- **AC13 (release artifacts).** Оба changelog обновлены в том же коммите
`0e5ee03` (User-Visible: yes), `docs/USER-GUIDE.md`/`.ru.md`,
`docs/ARCHITECTURE.md`, `docs/TESTING.md` обновлены там же;
`check-docs.mjs --external` зелёный; reference screenshots
(`docs/images/*.png`, `screenshots.json`) обновлены тем же коммитом.
Терминология RU-текста («капсула», «жёлтая подложка» для `#F0A00C`) сверена с
уже установленными терминами в этом же файле (устройство pulse-активности
уже описывает тот же `#F0A00C` как «жёлтую подложку», `src/device-pulse.ts`
`DEVICE_ACTIVITY_AMBER = '#F0A00C'`) — не изобретена заново. **Доказано**
diff + `check-docs` + сверка терминологии чтением.
## Находки
Нет находок High или Medium — ни в скоупе, ни вне его.
**Low (снято, без правки).** Хендофф-комментарий автора заявляет «954 passed,
1 skipped»; фактический прогон в этой сессии на том же коммите — «955 passed,
0 skipped» (см. таблицу гейтов). Расхождение не воспроизводится и не влияет на
вердикт — авторитетен прогон, выполненный ревьюером на диапазоне ревью;
вероятная причина — иное окружение автора (не единственный canonical способ
получить число, `npm run inventory` тоже даёт 955). Правки не требует.
## Что проверено и корректно
- Единая система координат core/shell (общий `.device-shell` бокс + один
`.device-shell-frame` с константным inset) устраняет источник независимого
округления, названный в issue, — подтверждено и алгебраически, и
dense-matrix/painted-centroid browser-доказательством с работающим mutant.
- Effective-size arithmetic численно тождественна старому позднему `× 0.9` во
всех точках входа (default/explicit/kiosk/per-marker/preview/vacuum).
- Hover/action unification переиспользует существующую bubble-модель `.dev`
без нового кода действия — риск задвоения действия исключён по построению
(`_clickDevice` не смотрит на `e.target`), не только по smoke-доказательству.
- Lock security invariant (никогда не toggled с плана) не затронут: единственный
обработчик клика на `.oplock` остаётся «открыть карточку», существующие
lock-action/lock-invariant smoke не изменены и зелёные.
- Continuous marker LQI — тонкая, безопасная правка (делегирование в уже
протестированную `lqiColor()`), room fill/tooltip путь не переиспользует
`markerLqiColor` и не затронут.
- Vacuum-puck собственный MDI ratio (`0.68`) сознательно не тронут — сохранена
договорённость из SPEC-REVIEW-213-r1 (Low снят на этапе ТЗ).
- Три копии бандла синхронны, docs fingerprint свежий, трейлеры и оба
changelog на месте в одном коммите.
- 30 golden-сценариев, помеченных `different`, визуально сверены (не только
по счётчику): расхождение точно очерчено пикселями маркеров/lock-бейджей,
никакой утечки в стены/комнаты/tray/диалоги не обнаружено.
## Чего не проверял
- **Полный `npm run smoke` (154 файла).** Diff не касается poверхностей вне
device-face/opening-lock/LQI (planering, backup, furniture, sun/glow
вычислений и т.д.); прогнаны все targeted + все смоки, тронутые
переименованием класса. Расширение до полного набора — предрелизный гейт
(PROCESS.md §8), непропорционально объёму диффа.
- **`npm run golden:accept`.** Не входит в роль ревьюера кода (только релиз-
менеджер/владелец после полного Linux CI артефакта); увиденные 30
`different` — ожидаемое следствие AC1–AC5, не дефект.
- **`node scripts/mutation-gate.mjs` (полный набор).** Дорогой прогон
(пересборка на мутанта), по процессу — предрелизный, не гейт ревью; прогнан
только единственный мутант, названный ТЗ для этой задачи
(`device-visual-factor-removed`), и он ловит регрессию.
- **`npm run performance_smoke` и полный HA-харнесс (`pytest tests_backend`).**
Diff не касается `custom_components/**/*.py` — backend вне класса A/B этой
задачи. Performance-профиль — источник для AC12 явно указан как
«source review + pre-beta performance smoke»; source review выполнен,
инструментальная часть — по процессу пре-релизная.
- **Custom/rotated/state-swapped MDI на не-32/56/96 размерах** сверх того, что
покрывает существующий reference-матрикс в `smoke_device_icon_design.mjs` —
архитектурно один общий CSS-токен, специальный smoke не заведён отдельно от
общего пиксельного alignment-smoke; риск оценён как низкий чтением.
## Вердикт
Зелёный. AC1–AC13 доказаны либо автотестом, который умеет падать (unit,
mutation-proof, browser smoke с собственным mutant), либо прочтением с
явной пометкой там, где инструментальная проверка по процессу отложена до
пре-релизного гейта (AC11, частично AC12). Golden проверен диагностически на
этом (Linux) раннере и подтверждает, что видимые изменения ограничены ровно
заявленным скоупом.