22 KiB
CODE-REVIEW-464-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/464
- Ветка:
issue/464-zigbee-topology-layer - Материал:
git diff origin/dev...HEADна SHA276bf193a2bec40a3c2fc823d364cbda00ac10d5(провереноgit rev-parse HEADнепосредственно перед выводом — совпадает) - Трек: полный (issue переведён из ошибочной оценки
trivialв полный тем же комментарием аналитика; ТЗ и ревью ТЗ (SPEC-REVIEW-464-r1, зелёное) это отражают) - Заход: r1 · блокирующих циклов израсходовано 0 из 4
Скоуп
Диапазон origin/dev...HEAD, 4 коммита, 48 файлов. Продуктовый код:
src/houseplan-card.ts, src/hp-zigbee-topology-overlay.ts,
src/zigbee-topology-overlay-bridge.ts, src/styles/devices.styles.ts.
Тесты/инструменты: demo/smoke_zigbee_topology_hover.mjs (расширен),
scripts/mutation-gate.mjs (+7 мутантов). Документация: ARCHITECTURE, STATUS,
TESTING, USER-GUIDE EN/RU, UX-MODES, CHANGELOG EN/RU, specs/054, specs/457,
specs/README, screenshots fingerprint. Generated: dist/**,
custom_components/houseplan/frontend/** — соответствуют исходникам этого SHA
(перепроверено пересборкой, см. таблицу гейтов).
ТЗ docs/specs/464-zigbee-topology-layer-order.md задаёт три требования:
AC1 — однозначный визуальный порядок (endpoints > topology > остальное),
AC2 — ownership/lifecycle/camera invariants, AC3 — casing неизвестного LQI.
Как проверялось — гейты
| Гейт | Статус | Как |
|---|---|---|
typecheck / build |
✅ зелёный | Validate на точном SHA 276bf193: https://github.com/Matysh/houseplan-card/actions/runs/33994548645 (успех). Не перегонял отдельно — цена нулевая по правилу «дешёвые гейты уже подтверждены на этом SHA», но ниже пришлось выполнить npm run bundle:sync для собственного воспроизведения бага, и tsc --noEmit && rollup -c при этом прошли зелёным попутно; git status после — чисто, пересборка байт-в-байт совпала с закоммиченным dist/** |
npm test |
✅ зелёный | Тот же прогон Validate |
npm run build + сверка 3 копий бандла |
✅ зелёный | Validate; попутно пересобрал сам (см. выше), git status чист |
node scripts/check-docs.mjs |
✅ зелёный | Validate; diff трогает src/**, commit 276bf193 отдельно обновляет sourceFingerprint, все 10 imageSha256 побайтно не изменились — подтверждено чтением docs/images/screenshots.json |
node demo/smoke_zigbee_topology_hover.mjs (прямой выбор smoke-select.mjs) |
✅ зелёный при исполнении | Прогнал сам: node demo/smoke_zigbee_topology_hover.mjs → OK, все out.*/geometry_*/raster_* true |
Мутанты AC1 (7 шт., scripts/mutation-gate.mjs) |
✅ 1 из 7 выборочно перепроверен | node scripts/mutation-gate.mjs --id=zigbee-topology-endpoint-elevation-removed → «покраснел, как обязан». Остальные 6 не перегонял — цена невелика, но соразмерность гейта не требует всех при отсутствии оснований сомневаться; см. находку ниже о том, что сами мутанты и сам смок не покрывают найденный дефект |
npm run golden:verify |
не гонял отдельно | покрыт зелёным Linux golden в Validate-прогоне на этом SHA |
npm run benchmark:zigbee-topology, bundle:budget, no-new-any, process-gate --issues |
не гонял | заявлены зелёными автором в хендоффе, дешёвая часть уже подтверждена Validate; выборочно не перепроверял — вне периметра находки этого раунда |
python -m pytest tests_backend |
не гонял | diff не трогает custom_components/**/*.py |
| Собственная проверка вне списка гейтов | ⚠️ обнаружила High | page.mouse.move (реальный CDP-курсор, не синтетический dispatchEvent) поверх маркера d_light1 в работающей карточке — см. находку 1 |
Находки
High-1 — реальный hover курсора оставляет hovered-source МАРКЕР под топологией, а не над ней (нарушает AC1)
Файл: src/styles/devices.styles.ts:336-341
:host([data-pointer-hover]) .dev:hover,
.dev:focus-visible { z-index: 5; }
/* #464 */
.dev[data-hp-zigbee-topology-endpoint] { z-index: 8; }
Спецификация CSS-селекторов: :host([data-pointer-hover]) .dev:hover весит
(0,4,0) (:host(arg) = специфичность псевдокласса + специфичность arg,
плюс класс .dev и псевдокласс :hover снаружи), тогда как новое правило
.dev[data-hp-zigbee-topology-endpoint] весит только (0,2,0). Когда реальная
мышь физически находится над исходным (hovered) маркером — то есть всегда,
пока диагностика вообще видна, — этот же элемент одновременно матчит и старое
правило :hover (z-index 5), и новый атрибут endpoint (z-index 8). Специфичность
решает в пользу правила :hover: браузер применяет z-index: 5, а не 8.
Оверлей топологии стоит на z-index: 7 (src/hp-zigbee-topology-overlay.ts:50).
5 < 7 — линия/casing рисуются поверх маркера устройства, на которое
наведена мышь, ровно там, где ТЗ §7.1 и уточнение владельца требуют обратного
(«эти markers не должны перекрываться линиями»).
Смок smoke_zigbee_topology_hover.mjs не ловит это, потому что весь файл
эмулирует hover через el.dispatchEvent(new PointerEvent('pointerover', …))
(mouse() на строке 16-18) — а такой синтетический dispatchEvent не
переводит элемент в реальное состояние CSS :hover; это состояние управляется
внутренним hit-testing браузера по настоящим координатам курсора и включается
только настоящим перемещением указателя (Playwright page.mouse.move, CDP
Input.dispatchMouseEvent). Поэтому out.layerContract в смоке
(getComputedStyle(source).zIndex > overlay) видит несуществующее в реальном
использовании состояние — .dev:hover там никогда не матчит source, конфликт
не проявляется, и тест зелёный.
Воспроизведение (выполнено, не только прочитано):
// демонстрационный скрипт поверх собранной карточки (npm run bundle:sync,
// демо-харнесс demo/serve.mjs), полный listing доступен по запросу
await page.mouse.move(sourceRect.x, sourceRect.y); // настоящий курсор, не dispatchEvent
// … после ожидания отрисовки топологии …
Результат:
{
hostHasPointerHover: true,
sourceMatchesHover: true,
sourceHasEndpointAttr: true,
sourceZIndex: '5',
overlayZIndex: '7',
overlayHasLines: true
}
sourceHasEndpointAttr: true подтверждает, что JS-логика (_syncEndpointOwnership)
отработала правильно — атрибут стоит. Ломается именно CSS-каскад поверх него.
Контрольный прогон с фейковым .dev:hover, отключённым из уравнения
(el.matches(':hover') подтверждён отдельно sourceMatchesHover: true),
исключает альтернативное объяснение (например, что data-pointer-hover не
выставился) — расследование см. в истории сессии.
Сценарий отказа: J7 (docs/SCOPE.md) — «is my Zigbee mesh healthy here».
Admin наводит курсор на устройство на насыщенном плане, видит соседей. Линия и
её обводка стартуют ровно в центре этого маркера, поэтому часть иконки/shell
устройства, на которое наведена мышь, оказывается закрыта диагностической
линией — визуально наименее заметный курсором участок маркера (edge/badge)
может быть перекрыт полностью. Это прямо тот же класс дефекта, который #464
был заведён исправлять (только раньше маркер перекрывался целиком slibling'ом
.devlayer, теперь — частично, но именно у активного, наиболее важного
маркера — источника hover).
Почему не поймано мутантами: все 7 мутантов mutation-gate.mjs для этой
задачи патчат продуктовый код (значение z-index, состав endpoint-set, cleanup,
casing) и гоняются через тот же смок с синтетическим dispatchEvent. Ни один
мутант не имитирует реальный курсор, поэтому «тест умеет падать» доказано
только для мутаций кода, а не для этого класса реальных дефектов CSS-каскада —
защитный AC1 в части «источник поверх линии» не имеет свидетеля, отличного от
случая, где :hover физически не задействован.
Серьёзность: High. Блокирует — ломает центральный, явно
переподтверждённый владельцем визуальный контракт (комментарий владельца 2026-09-05: «выше названий комнат и посторонних маркеров, но ниже полных
маркеров устройств — концов активных связей») именно для случая, который
происходит при каждом использовании функции.
В скоупе задачи — правится в этом же issue, отдельный issue не заводится (решение владельца 2026-08-19, #202: находка в скоупе возвращает автору, а не уходит в новый issue).
Что проверено чтением и/или исполнением и корректно
- AC2 (ownership/lifecycle): прочитан весь
_syncEndpointOwnership/_clearEndpointOwnership/_setDesiredEndpointIdsцикл (src/hp-zigbee-topology-overlay.ts:189-238) — множество endpoint равно{hovered} ∪ {neighborMarkerId реально нарисованных линий}, remote/unplaced цели (bubbles) не входят — соответствует ТЗ §7.2. Смок подтверждаетexactEndpoints === 'd_lamp,d_light1,d_tv'при трёх известных фикстурах, иdomReplacementTransfersподтверждает перенос владения на замену DOM через boundedMutationObserver({childList:true}), которыйdisconnectedCallbackкорректно отключает (строка 151-153). Проверено и исполнением (сам смок зелёный), и чтением кода. - Live-camera single projection (ТЗ §7.4):
zigbee-topology-overlay-bridge.tsбольше не выставляетdata-hp-live-layer="camera"на дочернем оверлее (убрано в диффе), overlay — теперь ребёнок.devlayer, который несёt единственныйdata-hp-live-layer="camera". Смокgeometry_*_overlayHasNoProjection,geometry_*_liveAligned,geometry_*_settledAligned— все true на wide/tall × min/default/max. Подтверждено исполнением. - AC3 (casing): прочитан рендер
<line class="link-casing">перед<line class="link-core">— та же геометрия/dasharray/offset, ширина 4px против 2px = ровно 1px кромки с каждой стороны, стрелка/заливка route-arrow не тронуты, parent-route (bubble) без casing. Raster-проба смока (raster_casingInk,raster_transparentGaps) подтверждает и тёмную кромку, и сохранённый прозрачный промежуток — исполнением, не только чтением.forced-colorsправило (line { stroke: Highlight !important }) покрывает оба класса линий одинаково (голый тег-селекторline),forcedColorsPreservedв смоке — true. - Pointer-transparency и hit-testing сквозь оверлей:
pointer-events:noneна host и всех примитивах (включая новый селекторsvg, line, polygon, .halo, .remote, .parent-bubbleв static styles),out.pointerHitTargetв смоке подтверждает клик по постороннему маркеру достигает цели через слой. - Cleanup matrix: pointerleave/touch/pen/hover-gate-loss/mode-change/
space-change/setting-off/disconnect — каждый путь отдельно проверен смоком
(
leaveClears,touchClears,penClears,hoverGateClears,spaceChangeClears,editorHasNoOverlay,settingOffClears,runtimeInvalidationClears,disconnectClears) — все true при прогоне. - Документация: ARCHITECTURE/STATUS/TESTING/USER-GUIDE EN+RU/UX-MODES
описывают именно тот контракт, что в ТЗ, и ссылаются на #464; исторические
ТЗ #54/#457 получили явные пометки о замене части их разделов; CHANGELOG
EN+RU присутствуют в том же коммите (
565acb60), что и поведение — соответствует §2.6/§10.2.docs/specs/README.mdдата и таблица обновлены. - Скриншоты:
docs/images/screenshots.json— все 10imageSha256побайтно совпадают со старыми значениями, изменился толькоsourceFingerprint(+ерайдlastWriteWasFingerprintOnly), что соответствует утверждению «10 raster witnesses побайтно совпали»; коммит276bf193изолирован и несётUser-Visible: no— корректно, видимых изменений в самих кадрах нет. - Трейлеры и структура коммитов: все 4 коммита несут
Issue: #464и ровно одинUser-Visible: yes|no; продуктовый + генерируемый + тесты + документация идут одним коммитом (565acb60) дляUser-Visible: yes, что закрывает требование «оба changelog в том же коммите». - i18n: новых строк нет, словари не тронуты — подтверждено чтением диффа
(нет изменений в
src/i18n/**).
Чего не проверял
- Остальные 6 из 7 мутантов
mutation-gate.mjsдля #464 — не перегонял по отдельности; доверился хендофф-заявлению автора плюс выборочной проверке одного (zigbee-topology-endpoint-elevation-removed, красный как надо). Риск невелик — их логика механическая (замена строк), но полностью не исключаю, что один из шести не ловит то, что заявлен ловить. npm run benchmark:zigbee-topology,bundle:budget,no-new-any,process-gate.mjs --issues,golden:verify— не гонял отдельно, положился на зелёный Validate этого точного SHA (ссылка выше) и на хендофф-числа автора.- Полный набор из 205 смоков — не запускал; diff не расширяет периметр за
пределы topology/overlay/camera,
smoke-select.mjsв хендоффе называет толькоsmoke_zigbee_topology_hover.mjsкак прямое совпадение, дополнительно автор сам прогналsmoke_toggle_entity,smoke_pan_any_zoom,smoke_room_fit— не перепроверял их лично, разумных оснований подозревать регресс там нет (diff не касается toggle/pan/room-fit логики). - Инварианты модели (
npm run invariants) — не запускал: diff не трогает геометрию комнат, толщину стен,layout,marker.spaceилиopen_spans; затронутая геометрия — исключительно экранные пиксели topology-оверлея, вне периметра §254. - Не проверял поведение при реальном touch/pen устройстве за пределами
смок-эмуляции синтетических
PointerEvent({pointerType:'touch'|'pen'})— сама эмуляция pointerType здесь достоверна (тип события — явное поле, не завязано на реальный hit-testing браузера, в отличие от:hover), поэтому разница между смоком и реальным устройством здесь не той же природы, что в находке 1. - Не проверял визуально (скриншотом) итоговый вид линии над маркером в High-1 —
ограничился числовым
getComputedStyle(...).zIndex, который и есть единственный источник, управляющий порядком отрисовки; дополнительный скриншот ничего не добавил бы к доказательству.
Вывод
AC2 и AC3 реализованы корректно и доказаны исполняемыми тестами, которые умеют
падать (мутанты + raster-пробы). AC1 реализован корректно для соседних
(neighbor) маркеров, но не выполнен для маркера-источника при реальном
использовании мышью — единственном способе, которым функция вообще
активируется, — из-за конфликта специфичности CSS-селекторов. Это найдено не
чтением, а прямым запуском собранной карточки с настоящим курсором;
существующий смок и все семь его защитных мутантов эту дельту не видят,
поскольку сами построены на синтетическом dispatchEvent, который не создаёт
реального состояния :hover.
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 1 · Medium: 0
Материал раунда
- Ветка:
issue/464-zigbee-topology-layer, коммит276bf193a2be— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
9d3e8b18d97336980bc58d0291bff8dab97cbe79git log --all --format='%H %T' | grep 9d3e8b18d973 - ТЗ
docs/specs/464-zigbee-topology-layer-order.md, блобfbefda4d5cf5acf53895d037cf7b0a5a1cf484f2git log --all --find-object=fbefda4d5cf5acf53895d037cf7b0a5a1cf484f2 -- docs/specs/464-zigbee-topology-layer-order.md