mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 12:49:56 +00:00
Проверка (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
Issue: #457 User-Visible: no
287 lines
24 KiB
Markdown
287 lines
24 KiB
Markdown
# Код-ревью #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`):
|
||
|
||
```ts
|
||
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
|
||
```
|
||
|
||
---
|
||
|
||
<!-- material-anchors: сгенерировано конвейером (#414) -->
|
||
|
||
## Материал раунда
|
||
|
||
- Ветка: `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
|
||
```
|