Issue: #457 User-Visible: no
24 KiB
Код-ревью #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, SHAf3a1dd84) - Материал:
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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
834b0cc7db248f1750932d2bb4483d4ea68071a8git log --all --format='%H %T' | grep 834b0cc7db24 - ТЗ
docs/specs/457-zigbee-route-arrows.md, блоб2c8350e397b0192a8f0db3d1c3ee0bf9f5f6e1f6git log --all --find-object=2c8350e397b0192a8f0db3d1c3ee0bf9f5f6e1f6 -- docs/specs/457-zigbee-route-arrows.md