mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -0,0 +1,223 @@
|
||||
# CODE-REVIEW-687-r1
|
||||
|
||||
Issue: [#687](https://github.com/Matysh/houseplan-card/issues/687) — «Редактор плана: показывать значки устройств
|
||||
ориентирами — 1 в 1 как в редакторе подложки».
|
||||
Материал: `git log --oneline origin/dev..HEAD`, `git diff origin/dev...HEAD` на SHA
|
||||
`128af2392a3d75e75716a69271682bbf1775cc09` (рабочая копия уже на нём).
|
||||
Заход: r1 · блокирующих циклов израсходовано 0 из 2 (лёгкий трек, лимит 2).
|
||||
|
||||
## Скоуп диффа
|
||||
|
||||
Два коммита поверх `dev`@`cab4b415`:
|
||||
|
||||
- `560849a1` — поведение (`src/styles/plan.styles.ts`), новый смок
|
||||
`demo/smoke_plan_device_landmarks.mjs`, три мутанта в
|
||||
`scripts/mutation-registry.mjs`, правка комментария в
|
||||
`demo/smoke_feedback_v2.mjs`, `docs/UX-MODES.md`, `docs/DECOR-EDITOR.md`,
|
||||
`docs/USER-GUIDE.ru.md`, оба changelog. `User-Visible: yes`.
|
||||
- `128af239` — Node-свидетель трёх мутантов (`test/plan-device-landmarks.test.mjs`)
|
||||
вместо браузерного гварда (реестр на потолке 200/200, #659), смок переведён
|
||||
на `window.__hpTest`. `User-Visible: no`.
|
||||
|
||||
Продуктовый код меняется одним CSS-правилом:
|
||||
|
||||
```css
|
||||
.stage.markup .devlayer .dev { filter: opacity(0.35); }
|
||||
.stage.markup .devlayer .dev,
|
||||
.stage.markup .devlayer .dev *,
|
||||
.stage.markup .devlayer .dev::before { pointer-events: none; }
|
||||
```
|
||||
|
||||
вместо прежнего `.stage.markup .devlayer .dev { display: none; }`. Обработчики
|
||||
маркера (`_pointerDown`, `_clickDevice`, `_keyDevice`, `_ctxDevice`, hover-tip,
|
||||
`interactive`/`tabindex`) не тронуты — уже закрыты вне `view`/`devices`.
|
||||
|
||||
Задача из `docs/SCOPE.md`: J6 «Keep the plan true as the home evolves» — расстановка
|
||||
стен/проёмов/лестниц относительно устройств без переключения в View; узкая,
|
||||
не расширяет скоуп.
|
||||
|
||||
## Как проверялось
|
||||
|
||||
Дешёвые гейты подтверждены зелёным Validate на этом SHA (run 36389513894 /
|
||||
36389932937, ссылка в задаче) — `npx tsc --noEmit`, `npm test`, `npm run build`
|
||||
+ `bundle-policy --verify` не перегонялись отдельно (#343). `heavy`-профиль
|
||||
Validate (браузерные смоки) на обычном push веток задач не запускается
|
||||
(`validate.yml` `changes.heavy`), поэтому смоки — моя обязанность в этом цикле.
|
||||
|
||||
`node scripts/smoke-select.mjs --base origin/dev --head HEAD` → **НЕОПРЕДЕЛЁННОСТЬ**
|
||||
(диф — чистый CSS, ни один смок не связан доказуемо; выборка не отменяет
|
||||
суждения). Прогнал сам смоки, названные в AC, плюс регрессию:
|
||||
|
||||
| Гейт | Команда | Результат |
|
||||
|---|---|---|
|
||||
| Build (для актуального `dist`/`test-build`) | `npm run build`, `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`, `node scripts/bundle-sync.mjs` | OK |
|
||||
| Node-свидетель каскада (AC1 п.2, AC2, AC3 scoping) | `node --test test/plan-device-landmarks.test.mjs` | 4/4 OK |
|
||||
| Смок AC1+AC2 (новый) | `node demo/smoke_plan_device_landmarks.mjs` | OK, все 24 поля `true` |
|
||||
| Регрессия AC3 (кнопка настроек комнаты) | `node demo/smoke_feedback_v2.mjs` | OK |
|
||||
| Регрессия ориентиров подложки (#362) | `node demo/smoke_decor.mjs` | OK |
|
||||
| Мутант `plan-device-landmarks-hidden` | `node scripts/mutation-gate.mjs --id=plan-device-landmarks-hidden` | поймано 1/1 |
|
||||
| Мутант `plan-device-landmarks-override-own-opacity` | `node scripts/mutation-gate.mjs --id=plan-device-landmarks-override-own-opacity` | поймано 1/1 |
|
||||
| Мутант `plan-device-landmarks-hit-target` | `node scripts/mutation-gate.mjs --id=plan-device-landmarks-hit-target` | поймано 1/1 |
|
||||
| Реестр мутантов на потолке | `node --test --test-name-pattern="#659: browser guard inventory" test/mutation-gate.test.mjs` | OK, 200/200 |
|
||||
| Монолит не читается новым тестом как текст | `node --test test/monolith-text-anchors.test.mjs` | OK — `plan-device-landmarks.test.mjs` не попал в новые якоря (читает `test-build/styles.js`, не `.ts`-исходник) |
|
||||
| Процессный гейт диапазона | `node scripts/process-gate.mjs --range origin/dev..HEAD` | 0 предупреждений (проверка 8 «статус issue» не запускалась — не требуется вне `--issues`) |
|
||||
| Документация | `node scripts/check-docs.mjs --screenshots=warn` | passed; WARN об отпечатке скриншотов — ожидаемо (сдвигает любая правка `src/**`), закрывается на кандидате |
|
||||
| Реестр мутантов: синтаксис/coverage | `node scripts/mutation-registry.mjs --check`, `node scripts/check-inputs.mjs --coverage` | OK |
|
||||
| entry-cost | `node --test test/entry-cost.test.mjs` | 3/3 OK |
|
||||
|
||||
После прогона `npm run build` дерево вернул `npm run bundle:clean`
|
||||
(`git status` чист) — в репозиторий ничего не писал.
|
||||
|
||||
## Разбор AC
|
||||
|
||||
**AC1 — видимость и непрозрачность.** Прочитан код: `.stage.markup .devlayer
|
||||
.dev { filter: opacity(0.35) }` не трогает `opacity` каждого маркера, поэтому
|
||||
`.dev.unavail` (собственная `opacity:.35`) даёт итог `0.35 × 0.35`, как того
|
||||
требует контракт. Смок подтверждает это на живом DOM:
|
||||
`planShowsViewMarkers` (множество `data-id` плана = View),
|
||||
`planOpacityMatchesBackground` (посчитанная альфа плана и подложки совпадает
|
||||
для каждого видимого маркера, включая один искусственно сделанный
|
||||
`unavailable`), `planPlainMarkerAt35` (0.35 ± 1e-3), `planUnavailableMarkerAt35x35`
|
||||
(0.35² ± 1e-3). Чем краснеет: мутант `plan-device-landmarks-hidden` возвращает
|
||||
`display:none` — красит Node-тест 1; мутант `-override-own-opacity` заменяет
|
||||
`filter` на `opacity` — красит Node-тест 2 (и увёл бы смок с `.unavail` на 35%
|
||||
вместо 12.25%). Оба поймано 1/1 на текущем HEAD.
|
||||
|
||||
**AC2 — полная неинтерактивность.** Прочитан код: обработчики `_pointerDown`,
|
||||
`_clickDevice`, `_keyDevice`, `_ctxDevice`, `pointerover`-tip уже гейтятся
|
||||
`this._mode !== 'view' && this._mode !== 'devices'` — эта часть контракта не
|
||||
менялась в диффе, только стала наблюдаемой (маркер раньше был `display:none`,
|
||||
то есть даже без событий). `interactive = mode === 'view' || 'devices'` держит
|
||||
`tabindex`/`role` не выставленными в Plan. Смок доказывает это исполнением, а
|
||||
не только чтением: реальный клик мышью (`page.mouse.click`) в центр видимого
|
||||
маркера добивает точку в цепочку «Стен» (`wallsToolReceivesClickThroughMarker`),
|
||||
реальный клик в маркер над кнопкой настроек комнаты открывает диалог комнаты
|
||||
(`roomSettingsOpenThroughMarker`) — то есть контракт «маркер не владеет точкой»
|
||||
проверен не только `elementFromPoint`, но и настоящим browser-событием. Прямая
|
||||
диспетчеризация `pointerover/pointerdown/pointerup/click/contextmenu` на сам
|
||||
DOM-узел маркера (события, которые `pointer-events:none` не блокирует, так как
|
||||
это programmatic dispatch, а не hit-test) не меняет `_tip`/`_infoCard`/
|
||||
`_deviceDrag`/`_selId`, не вызывает `callService`/`callWS`, не ставит
|
||||
`data-hp-device-hover`, `contextmenu` не помечен `defaultPrevented`. Чем
|
||||
краснеет: мутант `plan-device-landmarks-hit-target` снимает блок
|
||||
`pointer-events:none` — красит Node-тест 3 (в браузере вернул бы захват клика
|
||||
маркером); поймано 1/1.
|
||||
|
||||
**AC3 — подписи комнат и границы режимов.** Прочитан код: правило непрозрачности
|
||||
и правило неинтерактивности в диффе явно скопированы на `.dev`, а не на весь
|
||||
`.devlayer` — `.roomlabel` (сосед `.dev` внутри того же `.devlayer`) сохраняет
|
||||
собственное `.stage.markup .roomlabel { pointer-events: auto; }` (строка 686,
|
||||
не менялась) и не получает `filter`. Смок: `roomLabelNotFaded` (эффективная
|
||||
альфа подписи равна её собственной `opacity`, без множителя из `.dev`-правила),
|
||||
регрессия `demo/smoke_feedback_v2.mjs` (кнопка настроек читаема и открывает
|
||||
диалог) и `demo/smoke_decor.mjs` (ориентиры подложки #362 не задеты) — зелёные.
|
||||
Node-тест 4 (`the landmark fade is scoped to the Plan editor stage`) пинует,
|
||||
что всякое правило с `filter: opacity(0.35)` над `.dev` начинается с
|
||||
`.stage.markup ` — View/Devices/kiosk не затронуты. «Правила ограничены
|
||||
`.stage.markup`» проверено чтением (единственное новое правило) и Node-тестом
|
||||
одновременно.
|
||||
|
||||
Ни один AC не остался «доказан заявлением»: у каждого есть либо исполненный
|
||||
браузерный смок, либо Node-тест с подтверждённой мутацией, либо обе проверки
|
||||
вместе.
|
||||
|
||||
## Продуктовое рассуждение
|
||||
|
||||
Сценарий закрыт буквально: администратор видит устройства в редакторе плана
|
||||
теми же значками/на тех же местах, что и в View, и ни один инструмент плана
|
||||
(«Стены», настройки комнаты) не блокируется маркером — проверено реальным
|
||||
кликом мыши, а не только `elementFromPoint`. Решение владельца «1 в 1 как в
|
||||
подложке» перенесено без отклонений от контракта; единственное сознательно
|
||||
задокументированное отличие (в блоке «Принято предположительно») — множитель
|
||||
через `filter` на каждом `.dev`, а не групповой `opacity` на `.devlayer`, по
|
||||
объективной причине (подписи комнат — тот же слой). Риски (визуальное
|
||||
перекрытие превью инструментов маркерами, сдвиг golden) названы в ТЗ и приняты
|
||||
владельцем заранее; ничего нового не всплыло при чтении кода.
|
||||
|
||||
## Находки
|
||||
|
||||
Ничего блокирующего. Одно наблюдение Low, снимаю сам без возврата автору:
|
||||
|
||||
- **Low (снято без правки).** Значение 0.35 — не единый источник: в
|
||||
`src/houseplan-card.ts:10868` оно живёт в тернарнике
|
||||
`this._mode === 'decor' ? 0.35 : 1` (Background), в `plan.styles.ts:1186`
|
||||
(новая строка) — отдельным литералом `filter: opacity(0.35)` для Plan. Прямого
|
||||
разделяемого источника (общей CSS-переменной/константы) нет; изменение
|
||||
значения для Background потребует отдельно вспомнить про Plan. Не поднимаю до
|
||||
Medium: (а) числовое равенство проверяется исполнением на каждом прогоне —
|
||||
`planOpacityMatchesBackground` в новом смоке сравнивает вычисленные (а не
|
||||
захардкоженные) альфа-значения plan/decor и упадёт при любом расхождении;
|
||||
(б) унификация потребовала бы также переписать вычисление CSS-переменной в
|
||||
TS-тернарнике на добавление `'plan'` в условие — это уже не CSS-правка одной
|
||||
сцены, вне соразмерного объёма лёгкой задачи (PROCESS.md §8). Риск дрейфа
|
||||
реален, но пойман автоматически, а не тихо.
|
||||
|
||||
## Что проверено и корректно
|
||||
|
||||
- Контракт п.1–5 ТЗ — see AC-разбор выше; смоки и Node-тесты исполнялись
|
||||
реально на этом SHA, не переиспользовались вслепую.
|
||||
- Три мутанта регистрируют реальную защиту (guard красный на исходном
|
||||
правиле, зелёный после патча) — не «тест умеет падать» без названной
|
||||
мутации: команды и результат приведены в таблице гейтов.
|
||||
- Документация (`UX-MODES.md`, `DECOR-EDITOR.md`, `USER-GUIDE.ru.md`, оба
|
||||
changelog) обновлена в том же коммите, что и поведение (`560849a1`,
|
||||
`User-Visible: yes`) — трейлеры соблюдены.
|
||||
- `test/plan-device-landmarks.test.mjs` не входит в замороженный список тестов,
|
||||
читающих монолит как текст (#624) — подтверждено исполнением
|
||||
`monolith-text-anchors.test.mjs`; читает скомпилированный `test-build/styles.js`,
|
||||
а не `src/houseplan-card.ts`.
|
||||
- Обработчики маркера, `interactive`/`tabindex`, `_ctxDevice` — прочитаны в коде
|
||||
и подтверждают заявление «как сейчас» из тела issue дословно (номера строк
|
||||
совпадают с точностью до текущего HEAD).
|
||||
- `DEVICE-PRESENTATION.md` не требует правки — описывает анатомию маркера, а не
|
||||
видимость по режимам; в диффе анатомия не меняется.
|
||||
- Магнитные цели (контракт п.3, последний пункт «маркер не цель привязки») —
|
||||
не проверены новым смоком отдельно, но это не регрессия: `DECOR-EDITOR.md`
|
||||
фиксирует, что магнит и раньше исключал устройства («The image, devices and
|
||||
openings are excluded»), а привязка в редакторе плана считается по геометрии
|
||||
комнат/стен, не по DOM-хиттесту маркеров, — видимость маркера эту логику не
|
||||
затрагивает.
|
||||
|
||||
## Чего не проверял
|
||||
|
||||
- `typecheck`/`npm test`(полный)/`npm run build` со сверкой бандла как отдельный
|
||||
прогон — приняты по зелёному Validate на этом SHA (#343); build я всё же
|
||||
выполнил локально (для актуализации `test-build`/`dist` под смоки) и он
|
||||
зелёный, но не сверял три копии бандла (`bundle-policy --verify`) отдельно —
|
||||
не требуется, дифф не трогает `src/**` за пределами стилей и не относится к
|
||||
выпуску.
|
||||
- `npm run golden:verify` — не гонял. Дифф видимо меняет рендер (маркеры теперь
|
||||
рисуются в `.stage.markup`), кадры golden редактора плана с устройствами
|
||||
ожидаемо разойдутся — это named риск в ТЗ, и ТЗ прямо относит их приёмку на
|
||||
предрелizный гейт коммитом с `Release:` (`golden:accept --reviewed
|
||||
--expect-change=<id…>`, §11.4). Гонять `golden:verify` в этом цикле означало
|
||||
бы зафиксировать заведомо ожидаемое расхождение как «находку» — не гейт этого
|
||||
ревью.
|
||||
- `python -m pytest tests_backend` — не гонял, диф не трогает
|
||||
`custom_components/**/*.py`.
|
||||
- `npm run invariants` — не гонял, диф не меняет геометрию модели или ссылки на
|
||||
неё (только CSS-презентация).
|
||||
- Performance-профили — не названы в AC, не гонял.
|
||||
- Полный `npm run gate:small` целиком — гонял точечные его составляющие
|
||||
(сборка, Node-тест, целевые смоки, process-gate, check-docs,
|
||||
mutation-registry/check-inputs, entry-cost); не гонял полный набор смоков
|
||||
матрицы (279 смоков) — вне объёма код-ревью на лёгком треке (§8, «Полные
|
||||
наборы — предрелизный гейт»).
|
||||
|
||||
## Материал раунда
|
||||
|
||||
`git rev-parse HEAD` на момент ревью: `128af2392a3d75e75716a69271682bbf1775cc09`.
|
||||
|
||||
---
|
||||
|
||||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||||
|
||||
## Материал раунда
|
||||
|
||||
- Ветка: `issue/687-plan-editor-device-landmarks`, коммит `128af2392a3d` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
|
||||
- Дерево материала: `8e3f664c15634ec66041bba978ed865101bfd4f8`
|
||||
```
|
||||
git log --all --format='%H %T' | grep 8e3f664c1563
|
||||
```
|
||||
- Тело issue: `266af7ca2b7a21e08ab181761390dc4bab1425862dc2c6e48811d7777da34daa`
|
||||
- Вердикт конвейера: `green` · High 0
|
||||
Reference in New Issue
Block a user