Files
houseplan-card/docs/reviews/CODE-REVIEW-457-r2.md
T
claude[bot] 7320937f62
Проверка (CI) / Классификация изменённых файлов (push) Successful in 31s
Проверка (CI) / HACS: валидация репозитория (push) Skipped
Проверка (CI) / Hassfest: манифест интеграции (push) Skipped
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Skipped
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 43s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 47s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Skipped
docs: review document for #457
Issue: #457
User-Visible: no
2026-09-05 08:07:26 +00:00

24 KiB
Raw Blame History

Код-ревью #457 — направление Zigbee-связей к координатору (r2)

  • Issue: https://github.com/Matysh/houseplan-card/issues/457
  • ТЗ: docs/specs/457-zigbee-route-arrows.md (принято зелёным в docs/reviews/SPEC-REVIEW-457-r1.md, SHA f3a1dd84)
  • Материал: git diff origin/dev...HEAD, git log --oneline origin/dev..HEAD
  • SHA материала: d80805ac (ветка issue/457-zigbee-route-arrows)
  • Заход: r2 · блокирующих циклов израсходовано 1 из 4

Почему разбор полный, а не по дельте

Правило §2.9/§7.2 требует полный разбор, если «дельта не локальна: ребейз на ушедший вперёд dev». Именно это произошло: SHA r1 (d44aa30f, коммиты f3a1dd84/c844473c/ee3af3b8/e574bc6b) не существуют ни в одной ветке текущего репозитория —

git cat-file -t d44aa30f   → fatal: Not a valid object name
git cat-file -t c844473c   → fatal: Not a valid object name
git cat-file -t ee3af3b8   → fatal: Not a valid object name

Автор подтвердил это прямо в комментарии: «Ветка синхронизирована с актуальным dev». git merge-base origin/dev HEAD = b04e4aac — текущий tip dev, т.е. ветка действительно перебазирована. Содержимое коммитов при этом не изменилось (сообщения и диффы идентичны построчно тому, что цитирует документ r1 — сверено ниже), но это по правилу другой код, поэтому ниже — полный разбор всех 12 AC, а не только двух находок r1. Разделы «Закрытие раунда r1» и «Унаследовано из r1» в конце — обязательные для r2.

Скоуп диффа

git diff origin/dev...HEAD --stat: 53 файла. Продуктовые поверхности: src/zigbee-topology.ts (дерево аплинков BFS, нормализация relationship, проекция parent target), новый src/zigbee-topology-geometry.ts (screen-pixel геометрия наконечника), src/hp-zigbee-topology-overlay.ts (стрелки, bubble, forced-colors, переход viewBox с процентов на пиксели), src/zigbee-topology-overlay-bridge.ts + src/houseplan-card.ts (проброс spaces), 4 словаря src/i18n/topology/*.json. Доказательные поверхности: test/zigbee-topology.test.mjs, demo/smoke_zigbee_topology_hover.mjs, 4 записи в scripts/mutation-gate.mjs. Документация: оба CHANGELOG, оба USER-GUIDE, docs/STATUS.md, docs/specs/README.md, отпечаток скриншотов (docs/images/screenshots.json). dist/** и custom_components/houseplan/frontend/** синхронизированы (пересборка не изменила рабочее дерево). Backend Python не тронут.

Соответствует заявленному в ТЗ §16 объёму; неожиданных поверхностей нет.

9 коммитов на origin/dev (b04e4aac): a63aef02 (ТЗ), d13ecf12 (spec review doc), 793d06c7 (feat, Issue: #457/User-Visible: yes, оба changelog+STATUS+USER-GUIDE в этом же коммите), 58b16515 (отпечаток скриншотов), 41b8e712 (perf, User-Visible: no — этот коммит тихо откатил нормализацию relationship, см. ниже), da22b87f (отпечаток), 7cb1eee3 (code review r1), 896ceecf (fix по r1, User-Visible: no), d80805ac (отпечаток review r1). Трейлеры Issue/User-Visible на месте во всех коммитах.

Что и как проверял (гейты)

Зелёного Validate на SHA d80805ac не было, прогнал сам, на точном коде:

Гейт Результат
npx tsc --noEmit зелёный, без вывода
npm test 1980 passed, 1 skipped, 0 failed (полный прогон, test/zigbee-topology.test.mjs в их числе)
npm run build + git status --porcelain сборка чистая, рабочее дерево не поменялось — dist/** уже синхронизирован коммитом
npm run bundle:sync зелёный, обе поставляемые копии (dist/**, custom_components/houseplan/frontend/**) синхронны
npm run bundle:budget initial View 297 496 B gzip (потолок 298 000±2000, budget 300 000), headroom 2504 B — зелёный; предупреждение о низком запасе — существующий долг #367, не эта задача
node scripts/check-docs.mjs пройден (7 файлов, 12 внешних ссылок) — обязателен, диф трогает src/**
node scripts/no-new-any.mjs --base origin/dev --head HEAD новых any нет (252 добавленные строки, 5 файлов)
node scripts/smoke-select.mjs --base origin/dev --head HEAD одна зарегистрированная связь — demo/smoke_zigbee_topology_hover.mjs; 28 слабых связей по общему символу _mode, не по теме — не гонял
node demo/smoke_zigbee_topology_hover.mjs зелёный, все 22 поля checkAll true, включая 6 новых geometry_{wide,tall}_{min,default,max} (см. AC9)
npm run benchmark:zigbee-topology normalize 9.35 ms, map 22.64 ms, firstHover 0.72 ms, repeatedHover(20) 6.89 ms — все далеко внутри потолков 80/160/180/120 ms
npm run golden:verify пройден полностью (exit 0), падений нет — как и требует ТЗ §15.8 (новых baseline нет)
node scripts/mutation-gate.mjs --id=<name> × 4 (все новые мутанты #457) все 4 «покраснели, как обязаны» (см. таблицу AC ниже)
Инварианты модели (npm run invariants) не гонял — диф не трогает геометрию комнат/стен/layout/marker.space/open_spans, это runtime-проекция поверх уже размещённых устройств
python -m pytest tests_backend не гонял — custom_components/**/*.py не тронут
Полный node scripts/mutation-gate.mjs (без --id, с пересборкой бандла на каждого из тысяч мутантов) не гонял — это дорогой предрелизный гейт (§8), не гейт код-ревью; ограничился точечным --id на 4 новых мутанта задачи

Защитные AC — таблица «чем краснеет»

AC Чем доказан Чем краснеет
AC1 (дерево ведёт к координатору, distance-1 guard) test/zigbee-topology.test.mjs (uplink tree is deterministic…) node scripts/mutation-gate.mjs --id=zigbee-route-parent-keeps-bfs-level → красный, поймано 1/1
AC2/AC3 (нормализация relationship, разделители/регистр) test/zigbee-topology.test.mjs (Z2M relationship strings ignore case and separators) node scripts/mutation-gate.mjs --id=zigbee-route-relationship-separators-normalized → красный, поймано 1/1
AC4 (направление локальной стрелки не инвертировано) hover projects local route directions… --id=zigbee-route-local-arrow-not-inverted → красный, поймано 1/1
AC6 (parent не задвоен в remote_count) тот же тест --id=zigbee-route-parent-not-counted-twice → красный, поймано 1/1

Все четыре мутации прогнаны лично в этом раунде (не со слов автора).

Разбор по AC

AC1. buildZigbeeRouteTree (src/zigbee-topology.ts:326-357) строит BFS от единственного coordinator; parents заполняется только кандидатами с distances.get(neighborKey) === distance - 1 — цикл невозможен по построению. Адъяцентность строится по отсортированным ключам узлов/рёбер (adjacencyOf), поэтому результат не зависит от порядка входа — покрыто тестом с перестановками. Мутация guard'а ловится. Закрыт.

AC2. Tie-break (src/zigbee-topology.ts:349-356): прямое relationship==='parent' → прямой LQI (unknown как -1) → лексикографический ключ. LQI берётся из наблюдения самого узла о соседе (left.observation), т.е. не обратное — соответствует запрету ТЗ. Table-driven тест на ties и противоречивый relationship зелёный. Закрыт.

AC3. Z2M_RELATIONSHIPS — точная таблица 0…4 из ТЗ §6.3. Неизвестное число вне 0-4 даёт undefined (не роняет link/LQI, не становится предпочтением). Подтверждено реальной анонимизированной Z2M-фикстурой (relationship: 2 → sibling). Закрыт.

Находка r1 №1 (нормализация строк) — подтверждено закрытой. Текущая relationshipOf (src/zigbee-topology.ts:110-116):

function relationshipOf(value: unknown): string | undefined {
  if (typeof value === 'number') return Z2M_RELATIONSHIPS[value];
  if (typeof value !== 'string') return undefined;
  const compact = value.trim().toLowerCase().replace(/[\s_-]+/g, '');
  return compact === 'previouschild' ? 'previous_child' : compact.slice(0, 40) || undefined;
}

Регулярка [\s_-]+ снова на месте (её убрал 41b8e712, добавил обратно 896ceecf). Новый unit Z2M relationship strings ignore case and separators (test/zigbee-topology.test.mjs:115-130) кормит именно тот кейс, который раньше не проверялся: ' PREVIOUS-child ' → 'previous_child' и ' P_a-r ent ' (пробел/подчёркивание/дефис внутри строки) → 'parent'. Прогнал: тест зелёный, мутация zigbee-route-relationship-separators-normalized (откатывающая именно эту регулярку) краснеет. Разошедшееся с ТЗ поведение исчезло, регрессия закрыта тестом, который умеет её ловить.

AC4. resolveMappedTopologyHover (src/zigbee-topology.ts:398-431): isParent → toward-neighbor, обратное условие → toward-origin, иначе без стрелки. Координатор никогда не имеет parents.get(coordinatorKey), поэтому исходящей стрелки нет ни при каком соседе. Юнит + browser smoke (localRouteArrow) зелёные, мутация инверсии стрелки краснеет. Закрыт.

AC5. Путь построения lines не тронут кроме добавления routeDirection; цвет/пунктир/halo/дозаполнение LQI — прежняя логика. Регрессионные unit-и зелёные с добавленным полем parentTargets: []. Закрыт.

AC6. parentTargets строится до основного цикла и явно исключает parent из remote (if (!isParent) remote.add(...)). Название берётся из this.spaces (_targetText), пустой/нестроковый title не заменяется raw id — используется route_other_space. Мутация повторного учёта parent в remote count краснеет; browser smoke (remoteParentBubble) подтверждает и текст, и то, что старый агрегат zigbee-topology-remote не рисуется одновременно с bubble. Ровно тот случай «одно число — один источник» (remote-count не должен видеть parent дважды), и здесь у него один единственный путь подсчёта. Закрыт.

AC7. Классификация unplaced-coordinator vs unplaced-device — по роли узла в полном графе, не по причине ненаходимости; hidden/ambiguous/ unmatched/provider_scan_failure дают одинаковый unplaced-device (это гарантирует сама структура placements, которая строится только для drawable устройств — mapTopologyNodes/drawable(), src/zigbee-topology.ts:287-289: hidden, virtual, ha_disabled, orphaned, unverified все дают "не в placements", т.е. одинаковый текст). Bubble только для parent наведённого узла — неразмещённые дети не участвуют в parentTargets, только в omittedCount (нигде не выводится). DOM-privacy: raw IEEE/provider id не попадают в _targetText ни в одной ветке. Browser smoke (unplacedDeviceBubble, unplacedCoordinatorBubble) зелёный. Закрыт.

AC8. buildZigbeeRouteTree возвращает пустые parents/distances при coordinators.length !== 1; для нулевого координатора и множественного — отдельные unit. Недостижимые компоненты просто не попадают в distances, поэтому не получают parent — обычные линии продолжают работать, потому что resolveMappedTopologyHover не завязан на наличие дерева для построения lines. Закрыт.

AC9 — находка r1 №2, подтверждено закрытой. ТЗ требует «pure pixel-geometry unit + browser smoke на двух aspect ratios и трёх zoom». demo/smoke_zigbee_topology_hover.mjs теперь гоняет для {wide 1100×500, tall 480×900} × {zoom 1/3, 1, 8} (_applyView(zoom, …) — штатный, не-CSS-transform механизм houseplan-card.ts:6358, не тот [data-hp-live-layer= "camera"] transform, что применяется только во время переходной анимации камеры и снимается по завершении, live-viewport.ts:145-159), в каждой из 6 комбинаций проверяет через getScreenCTM() реальную экранную длину/полуширину наконечника (8.5 < length < 9.5, 4 < halfWidth < 5) и коллинеарность с линией (alignment > 0.999), плюс pointer-events:none. Прогнал лично: все 6 geometry_* полей — true. Это ровно тот класс проверки, которого не хватало в r1, и он теперь реально проходит на границах диапазона (min/max zoom), а не только на дефолте. Закрыт для заявленного в ТЗ диапазона.

Остаточное (не входит ни в один AC и не поднимаю выше информационного): r1 отдельно упоминал непроверенный сценарий «hover активен во время самого жеста pan/zoom мышью» (когда [data-hp-live-layer="camera"] transform применён к оверлею напрямую) — в отличие от проверенного здесь «zoom уже применился, жест завершён». Я не проверял этот сценарий и не заявляю его багом; см. «Чего не проверял».

AC10. .route-arrow/.parent-bubble в блоке forced-colors: active используют Highlight/CanvasText/Canvas. Lifecycle (pointerleave, mouse→touch, смена пространства/режима) не тронут отдельно — новые элементы рендерятся из того же render(). Smoke leaveClears/touchClears/ editorHasNoOverlay/nonAdminHasNoOverlay зелёные. Закрыт.

AC11. benchmark:zigbee-topology — все 4 метрики далеко внутри потолков (см. таблицу гейтов); mapMs уже включает построение дерева, так как buildZigbeeRouteTree вызывается внутри mapTopologies. bundle:budget зелёный. zigbee-topology-geometry.ts импортируется из ленивого модуля (zigbee-topology-overlay-bridge.ts → void import('./hp-zigbee-topology-overlay')), initial View не растёт. Закрыт.

AC12. Все 4 словаря получили одинаковые 3 ключа (route_device_not_on_plan, route_coordinator_not_on_plan, route_other_space); check-docs.mjs зелёный; оба USER-GUIDE описывают семантику стрелки, approximation и честное отсутствие пути ровно теми словами, что требует ТЗ §19; оба CHANGELOG содержат одну пользовательскую запись со ссылкой на #457 в том же коммите (793d06c7, User-Visible: yes), без утечки внутренних терминов; docs/STATUS.md обновлён. Закрыт.

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
Medium №1 — нормализация relationship откатилась (ee3af3b8/41b8e712 убрал .replace(/[\s_-]+/g, '')) Коммит 896ceecf вернул регулярку и специальный случай previouschild → previous_child; добавлен unit Z2M relationship strings ignore case and separators, кормящий именно разделённые строки; добавлен mutation witness zigbee-route-relationship-separators-normalized src/zigbee-topology.ts:113; test/zigbee-topology.test.mjs:115-130; scripts/mutation-gate.mjs (id найден, прогнан лично — красный на мутации)
Medium №2 — AC9 доказан не полностью (нет browser smoke на aspect ratio/zoom) Коммит 896ceecf добавил матрицу {wide, tall} × {min, default, max zoom} в demo/smoke_zigbee_topology_hover.mjs с реальными пиксельными assertions через getScreenCTM() demo/smoke_zigbee_topology_hover.mjs:157-220; прогнан лично — все 6 geometry_* полей true

Обе находки r1 были Medium, в скоупе — обе исправлены в той же ветке, ни одна не переоткрылась в другом виде.

Унаследовано из r1

Ничего — этот раунд полный по правилу §7.2 (ребейз), поэтому каждый AC1–AC12 и обе находки r1 перепроверены заново в этом документе с указанием команды и результата, а не унаследованы со ссылкой на документ r1 без повторной проверки. Документ r1 (docs/reviews/CODE-REVIEW-457-r1.md, SHA d44aa30f) использован только как источник контекста находок и текста AC-анализа для сверки — не как источник доверия к результатам гейтов.

Низкие / снятые без правки

  • Не найдено новых. Old-Low из r1 (.route-arrow { vector-effect: non-scaling-stroke } без stroke, эффекта не имеет) не тронут этим раундом, остаётся тем же безвредным мёртвым правилом; повторно смотрел — вывод не изменился, продолжаю снимать без правки.

Чего не проверял

  • python -m pytest tests_backend — custom_components/**/*.py не тронут.
  • npm run invariants — диф не меняет геометрию комнат/стен/layout/ marker.space/open_spans, только runtime-проекцию поверх уже размещённых устройств.
  • 28 «слабых» смок-связей по общему символу _mode (smoke-select.mjs) — не по теме этой фичи, не гонял.
  • Полный node scripts/mutation-gate.mjs (без --id, с пересборкой бандла на каждого из тысяч мутантов) — дорогой предрелизный гейт, не гейт код-ревью; прогнал только 4 новых мутанта задачи через --id.
  • Живой zoom/pan-жест мышью с активным hover во время самого жеста (когда [data-hp-live-layer="camera"] transform применён к оверлею напрямую, не после его завершения) — тот же непроверенный сценарий, что r1 оставил открытым информационно; не поднимаю его до находки, потому что он не входит ни в один принятый AC (AC9 требует min/default/max zoom состояний, не проверку в момент самого перетаскивания), и в r1 он тоже не был подтверждён как воспроизводимый баг.
  • Ручной браузер вживую (реальный HA, реальные ZHA/Z2M снапшоты) — вне цикла ревью по процессу; полагаюсь на unit + smoke + чтение кода.

Вердикт

Оба Medium из r1 закрыты предъявимо (тест + мутация/смок, не только заявление автора). Все 12 AC доказаны автотестом с проверенной способностью падать либо разобраны чтением с явной пометкой. Новых находок нет.

Вердикт: зелёный · заход r2 · блокирующих циклов 1/4 · High: 0 · Medium: 0

Материал раунда

  • Ветка: issue/457-zigbee-route-arrows, коммит d80805acec0e — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 834b0cc7db248f1750932d2bb4483d4ea68071a8
    git log --all --format='%H %T' | grep 834b0cc7db24
    
  • ТЗ docs/specs/457-zigbee-route-arrows.md, блоб 2c8350e397b0192a8f0db3d1c3ee0bf9f5f6e1f6
    git log --all --find-object=2c8350e397b0192a8f0db3d1c3ee0bf9f5f6e1f6 -- docs/specs/457-zigbee-route-arrows.md