Files
houseplan-card/legacy/reviews/v1.72.0/CODE-REVIEW-54-r1.md
T
Claudeandclaude[bot] 0991c45374 fix(tools): архив переписывает относительные ссылки перенесённых документов (#682)
Ревью #682 r1, Medium: перенос добавляет документу уровень вложенности
(`docs/reviews/X.md` → `legacy/reviews/<тег>/X.md`, `docs/specs/` →
`legacy/specs/`), а относительные ссылки внутри перенесённых документов и в
соседях, ссылавшихся на них, никто не пересчитывал — на `97d19268` 53 битые
ссылки в 46 файлах (заявление «все 26 резолвятся» в `7feb6177` было верно
только до переноса документов ревью). Гейты архив не смотрят.

`reviews-archive.mjs`: `repairLinks` пересчитывает ссылку, если она не
резолвится от нового места, а цель находится от нового или старого места
через карту переносов; битая и до переноса ссылка не трогается. `--apply`
делает это само, `--repair-links=<rev>` — для всех переименований
`<rev>..HEAD`, `--check-links` печатает битые. Этим коммитом
`--repair-links=origin/dev` переписал ровно 53 ссылки в 46 файлах; остались
две прежние «...»-заглушки в CODE-REVIEW-448-r2 (битые и на dev). Тесты:
перенесённый документ, сосед со ссылкой в архив, ТЗ со ссылкой на позже
перенесённое ревью, битая-до-переноса не трогается, в `legacy/` битых нет;
мутант `reviews-archive-links-from-new-place-only`. PROCESS §2.10 и
DEVELOPMENT › Release называют переписывание и `--check-links`.

Issue: #682
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-27 22:10:47 +00:00

288 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CODE-REVIEW-54-r1 — #54: контекстные связи Zigbee на плане (hover overlay)
- Issue: [#54](https://github.com/Matysh/houseplan-card/issues/54)
- Этап: `code` (PROCESS.md §2.7)
- Диапазон: `origin/dev...HEAD`, `origin/dev` = `ec9824f2`, `HEAD` = `a2867df9`
(ветка `issue/54-zigbee-topology-hover`, детач `HEAD`)
- ТЗ: [`docs/specs/054-zigbee-topology-overlay.md`](../../specs/054-zigbee-topology-overlay.md),
ревью ТЗ зелёное на заходе r2 — [`SPEC-REVIEW-54-r2.md`](SPEC-REVIEW-54-r2.md)
(r1 → M1/L1-L3 закрыты правкой `aed0b5b6`, подтверждено r2 зелёным)
- Заход: **r1** код-ревью (первый заход этого этапа для #54; ревью ТЗ и код-ревью
считаются отдельно — см. историю комментариев issue)
- Блокирующих циклов израсходовано: 0/4 до этого захода
- Вердикт: **жёлтый**
Это первый заход код-ревью #54 — полный разбор, раздел «дельта/унаследовано»
(PROCESS.md §2.10) не применяется.
## Скоуп ревью
60 файлов, +3433/−627 (`git diff origin/dev...HEAD --stat`).
Продуктовый код (класс A): `src/types.ts` (+2), `src/houseplan-card.ts` (+1
импорт, +1 render seam), `src/houseplan-editor-runtime.ts` (+19/-11, General
Settings wiring), новые модули `src/zigbee-topology.ts` (332),
`src/zigbee-topology-runtime.ts` (234), `src/zigbee-topology-settings.ts` (45),
`src/zigbee-topology-overlay-bridge.ts` (22), `src/hp-zigbee-topology-overlay.ts`
(203), `src/hp-zigbee-topology-settings.ts` (208), `src/i18n/topology.ts` (16) и
`src/i18n/topology/{en,ru,de,fr}.json` (по 27 ключей).
Гейты/инструменты (класс B): `test/zigbee-topology.test.mjs` (184, 9 тестов/10
subtests), `demo/smoke_zigbee_topology_hover.mjs` (102, 12 проверок),
`demo/benchmark_zigbee_topology.mjs` (47), `scripts/mutation-gate.mjs` (+4
мутанта), `scripts/config-field-registry.mjs` (+1 запись), `scripts/smoke-links.mjs`
(+1 связь), `package.json` (+1 npm-скрипт), `tsconfig.test.json`,
`tests_backend/test_support_package.py` (+regression-тест на исключение
`zigbee_topology` из support-пакета).
Документация (класс C): `docs/ARCHITECTURE.md`, `docs/CONFIG-COMPATIBILITY.md`,
`docs/SCOPE.md` (J7), `docs/SUPPORT-PRIVACY.md`, `docs/UX-MODES.md`,
`docs/USER-GUIDE.md`/`.ru.md`, оба `docs/CHANGELOG*.md`, `docs/images/screenshots.json`
(fingerprint).
Backend Python (`custom_components/houseplan/**/*.py`) — **не тронут**
(`git diff origin/dev...HEAD -- 'custom_components/**/*.py'` пуст), что
совпадает с явным заявлением ТЗ §15: House Plan backend не добавляется.
Сгенерированное (класс D): `dist/**`, `custom_components/houseplan/frontend/**`
— сверено `bundle:sync`, диф молчит после пересборки (см. таблицу гейтов).
Прочитано до вердикта: `docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md` (включая
§2.7, §2.10, §4, §7.2, §8), тело issue #54 и все 10 комментариев (аналитика
14.08 и 30.08, Stage 0 research, решение владельца 04.09, оба захода
SPEC-REVIEW, хендофф реализации), `docs/specs/054-zigbee-topology-overlay.md`
целиком, `docs/UX-MODES.md`, `docs/CONFIG-COMPATIBILITY.md`,
`docs/SUPPORT-PRIVACY.md`, весь новый и изменённый продуктовый код построчно
(`src/zigbee-topology.ts`, `src/zigbee-topology-runtime.ts`,
`src/zigbee-topology-settings.ts`, `src/zigbee-topology-overlay-bridge.ts`,
`src/hp-zigbee-topology-overlay.ts`, `src/hp-zigbee-topology-settings.ts`, дифы
`houseplan-card.ts`/`houseplan-editor-runtime.ts`/`types.ts`), `test/zigbee-topology.test.mjs`
целиком, `custom_components/houseplan/diagnostics.py` и `support_package.py`.
## Как проверялось
`npm ci` не выполнялся — зависимости и Chromium уже установлены средой
(рабочая копия чистая на входе).
| Гейт | Команда | Результат |
|---|---|---|
| Типы | `npx tsc --noEmit` | green, код 0 |
| Unit | `npm test` | **1905/1906 green, 1 skipped, 0 failed** (включая 10 subtests `test/zigbee-topology.test.mjs`) |
| Сборка | `npm run build` | green |
| Синхронность бандлов | `cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js` + `npm run bundle:sync` (нулевой diff после) | идентичны байт-в-байт |
| Документация/скриншоты | `node scripts/check-docs.mjs` (обязателен: diff трогает `src/**`) | green — `Documentation checks passed (7 files, 12 external links)`; `docs/images/screenshots.json`.`sourceFingerprint` уже обновлён в этом диапазоне (коммит `a2867df9`) |
| Новый `any` | `node scripts/no-new-any.mjs --base origin/dev --head HEAD` | green — 1074 добавленных строк в 10 файлах, новых `any` нет |
| Выбор смоков | `node scripts/smoke-select.mjs --base origin/dev --head HEAD` | 19 «прямое совпадение» (включая целевой `smoke_zigbee_topology_hover.mjs`), 23 «слабая связь», 1 «зарегистрированная связь» — см. раздел ниже |
| Целевой smoke | `node demo/smoke_zigbee_topology_hover.mjs` (запущен лично) | green — 12/12 полей `true` |
| Performance benchmark | `npm run benchmark:zigbee-topology` (запущен лично) | green: normalizeMs 9.88 (бюджет 80), mapMs 6.06 (160), firstHoverMs 1.22 (180), repeatedHoverMs 5.95 (120) на 500 узлах/2979 связях |
| i18n parity | `node -e` diff ключей `en/ru/de/fr` topology-словарей (запущен лично) | green — 27/27/27/27, missing/extra пусты для всех трёх переводов |
| Config registry/audit | `node scripts/config-audit.mjs` | green — `settings.zigbee_topology` зарегистрирован (`status: current`), без ошибок |
| Модельные инварианты | не прогонялся | diff не трогает геометрию/`layout`/`marker.space`/`open_spans`/рёбра комнат — гейт неприменим по построению (overlay только читает уже спроецированные экранные координаты существующих markers) |
| Backend pytest | не прогонялся | diff не трогает `custom_components/**/*.py` — гейт пуст по построению; единственная затронутая backend-строка — уже существующий тест `tests_backend/test_support_package.py`, прочитан вручную |
| Полный smoke-матрикс (221) | не прогонялся локально целиком | CI Validate на точном SHA `a2867df9` (ссылка ниже) прогнал все 3 шарда + сводный job — все green; смысла дублировать 221 браузерный тест локально при уже зелёном CI на этом SHA нет |
| Golden | не прогонялся локально | CI Validate на этом SHA: job `golden` **red** — 3 существующих кадра общих настроек (`general-color-popover-desktop-en`, `settings-help-zoom-200-en-light`, `settings-help-zoom-200-ru-dark`) сдвинулись из-за нового блока Zigbee; автор эталоны не принимал. Согласно PROCESS.md §8 полный golden — предрелизный гейт, а принятие делает `npm run golden:accept -- --reviewed` на CI-артефакте перед бетой. Локально не пересматривал сами PNG — доверяю описанию автора и списку из ровно трёх, топологически ожидаемых кадров (все из экрана «Общие настройки», куда и добавлена новая секция); не блокирует код-ревью |
CI Validate на SHA `a2867df9` (https://github.com/Matysh/houseplan-card/actions/runs/33869683826)
проверен через `gh run view` напрямую: `Фронтенд: типы/юниты/мутанты/синхрон
бандла` — success (это же исполнило 4 мутанта `zigbee-topology-*` из
`scripts/mutation-gate.mjs` и убедился, что все их guard-тесты красятся —
поэтому не повторял мутации из CI лично), `Смоки в браузере` (все 3 шарда +
сводный) — success, `Перф-смок` — success, `Golden` — failure (ожидаемо, см.
выше), `Hassfest`/`Бэкенд pytest`/`HACS` — skipped (путь не изменился).
### Мутанты — выборочно исполнены лично
`scripts/mutation-gate.mjs` содержит 4 записи `zigbee-topology-*`; все четыре
`find`-строки совпадают байт-в-байт с текущим кодом (не мертвы), и CI уже
подтвердил, что каждая красит `test/zigbee-topology.test.mjs`. Дополнительно
проверил лично два случая, не покрытых записями в `mutation-gate.mjs`:
1. **AC3 (permissions), `src/zigbee-topology-overlay-bridge.ts:17`.** Заменил
`input.hass?.user?.is_admin !== true` на `false` (снял admin-гейт рендера
оверлея), пересобрал (`npm run build && node scripts/bundle-sync.mjs`),
прогнал `node demo/smoke_zigbee_topology_hover.mjs` — поле `nonAdminHasNoOverlay`
покраснело (`expected true, got false`), остальные 11 остались зелёными.
Откатил файл, пересобрал, дерево снова чистое (`git status --short` пуст).
Guard реален и умеет падать.
2. **AC5 (no inferred route), `src/zigbee-topology.ts`, функция
`normalizeZhaTopology`.** Добавил блок, создающий связь из `record?.routes`
(используя существующее тестовое поле `routes: [{ dest_nwk, next_hop }]` из
`test/zigbee-topology.test.mjs:56`) в дополнение к `neighbors`. Пересобрал
тест-билд (`npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`)
и прогнал `node --test test/zigbee-topology.test.mjs` — тест «ZHA
normalization keeps directional observations and never infers route edges»
покраснел (`Expected values to be strictly equal: 2 !== 1`), остальные 8
остались зелёными. Откатил файл. ТЗ явно требует для AC5 «provider fixtures
+ mutation witness» (§13), а готовой записи в `mutation-gate.mjs` для этого
инварианта нет — восполняю пробел этим прогоном лично (для чистого юнита
PROCESS.md §2.7 считает это достаточным доказательством) и не считаю это
отдельной находкой, раз защита подтверждена и умеет падать.
## Критерии приёмки — таблица
| AC | Вердикт | Доказательство | Чем доказан | Чем краснеет |
|---|---|---|---|---|
| AC1 default/off | доказан | `zigbee-topology-overlay-bridge.ts:17-19`, `zigbee-topology-settings.ts:19-21` | unit (`zigbee-topology.test.mjs:33`) + smoke `defaultOff`/`offHasNoLazyChunk` | smoke `offHasNoLazyChunk` реально проверяет отсутствие сетевого ресурса; отдельно не мутировал — риск низкий, чтение подтверждает простую булеву цепочку |
| AC2 persistence | доказан | `zigbee-topology-settings.ts:37-45` (`writeZigbeeTopologySettings` мёрджит в `{...settings}`), `houseplan-editor-runtime.ts` save-path | unit `zigbee-topology.test.mjs:39-43` (сохраняет посторонний `keep:7` в обе стороны) + smoke `settingPersists` | unit явно проверяет неудаление соседнего поля; end-to-end (сохранение topology вместе с ДРУГИМ полем диалога через реальный `_saveSettingsDialog`) отдельно не смокировано — риск низкий, код читается прямолинейно (один общий `settings`-объект) |
| AC3 permissions | доказан | `zigbee-topology-overlay-bridge.ts:17`, `hp-zigbee-topology-settings.ts:84,102-103,144-152`, `zigbee-topology-runtime.ts:132-134` (`requireAdmin`, defence-in-depth на 3 уровнях) | unit (`readZhaTopology` denies non-admin) + smoke `nonAdminHasNoOverlay` | **лично воспроизвёл** — см. «Мутанты» выше, smoke красится |
| AC4 contextual only | доказан | `hp-zigbee-topology-overlay.ts:172` (рендер только при `_hovered`), `zigbee-topology.ts:296-322` (incident-only resolver) | unit + smoke `incidentOnly` | чтением + unit-таблица связей |
| AC5 no inferred route | доказан | `normalizeZhaTopology` не читает `record?.routes` нигде (grep пуст) | unit `zigbee-topology.test.mjs:52-64` (фикстура содержит `routes`, счёт связей = 1) | **лично воспроизвёл мутацию** — см. «Мутанты» выше, тест красится |
| AC6 same-space mapping | доказан | `zigbee-topology.ts:280,313` (exact `candidates.length===1`, same-space branch) | unit `zigbee-topology.test.mjs:82-101` | мутант `zigbee-topology-ambiguous-marker-selected` (CI green — красит guard) |
| AC7 cross-space truth | доказан | `zigbee-topology.ts:300,313` (`remote` — `Set`, дедуп по markerId), `hp-zigbee-topology-overlay.ts:193-196` (только count, без направления) | unit (`remoteCount:1`) + smoke `crossSpaceCount` | чтением; отдельная адверсариальная проверка мульти-провайдерного дедупа (два провайдера дают одного и того же remote-соседа) сделана в фоновом ревью подагентом вручную (см. §«Верифицировано подзадачами» ниже) — не задваивает count |
| AC8 ambiguous/hidden lifecycle | доказан | `zigbee-topology.ts:256-259` (`drawable()`), `:283` (`ambiguous_placement`) | unit `zigbee-topology.test.mjs:103-114` | мутант `zigbee-topology-ambiguous-marker-selected` (CI green) |
| AC9 modality/modes | доказан (частично чтением) | `zigbee-topology-overlay-bridge.ts:17` (`view`+`kiosk`+admin gate), `hp-zigbee-topology-overlay.ts:129-131` (`_mouseAllowed` требует `pointerType==='mouse'` И `data-pointer-hover`, которые никогда не ставит клавиатурный фокус), `space-card.ts` не содержит ссылок на zigbee (grep пуст) | smoke `touchClears`, `editorHasNoOverlay`, `nonAdminHasNoOverlay` | kiosk-режим, режимы `devices`/`decor` отдельно (только `plan` в smoke) и keyboard-focus не смокированы явно — **проверено чтением**: `pointer-modality.ts` ставит `data-pointer-hover` только по реальным PointerEvent, фокус клавиатуры в этот путь не попадает |
| AC10 cleanup | доказан (частично чтением) | `hp-zigbee-topology-overlay.ts:76-84` (`disconnectedCallback`), `:141-148` (`_pointerOut`), `:150-152` (`_pointerDown` non-mouse) | smoke `leaveClears`, `touchClears` | сценарий «выключить настройку прямо во время видимых линий» отдельно не смокирован — **проверено чтением**: выключение убирает элемент из рендера через bridge, что триггерит `disconnectedCallback` в тот же тик |
| AC11 pointer ownership | доказан | `hp-zigbee-topology-overlay.ts:41` (`:host{pointer-events:none}`, наследуется CSS-каскадом на все потомки, явных `pointer-events:auto` переопределений нет — grep пуст) | smoke `pointerTransparent` (реальный `getComputedStyle` на host и на `.remote`) | клик/пан/зум-регрессия не смокирована отдельным сценарием (spec §14 п.4 упоминает её) — **проверено чтением**: слой добавлен как sibling после существующей разметки, не оборачивает markers, `pointer-events` наследуется; структурного пути к перехвату клика нет |
| AC12 LQI visual | доказан | `hp-zigbee-topology-overlay.ts:185-189` (цвет по `lqiColor`, unknown → пунктир), `zigbee-topology.ts:314` (направление hovered→neighbor, без усреднения) | unit (случай `lqi: undefined`) | чтением; golden-сценарий заявлен в ТЗ §14.5, полный golden не прогонялся (см. таблицу гейтов) |
| AC13 ZHA contract | доказан | `zigbee-topology-runtime.ts:137-145` (только `zha/devices`, никогда `zha/topology/update`) | unit `zigbee-topology.test.mjs:116-136` | мутант `zigbee-topology-zha-read-starts-scan` (CI green) |
| AC14 Z2M contract | доказан с оговоркой | `zigbee-topology-runtime.ts:186-234` (transaction, timeout, retained/foreign фильтрация, cleanup в `finally`) | unit `zigbee-topology.test.mjs:138-184` | мутанты `zigbee-topology-z2m-foreign-response-accepted`, `zigbee-topology-z2m-subscriptions-leak` (CI green) — **но см. Medium M1**: путь «невалидный JSON на верном топике» не покрыт ни одним тестом и содержит реальный дефект |
| AC15 no implicit work | доказан | `zigbee-topology-runtime.ts:98-112` (`run()`, inflight `Map`) | unit `zigbee-topology.test.mjs:116-131` (2 конкурентных вызова → 1 `callWS`) | чтением + unit assert `calls===1` |
| AC16 failures | доказан с оговоркой | `zigbee-topology-runtime.ts:84-96` (`errorCode`/`fail`) | unit (permission, timeout пути) | **см. Medium M1** — `invalid_payload` код де-факто недостижим на response-топике Z2M |
| AC17 privacy | доказан | `zigbee-topology-settings.ts:1-4` (единственный персистируемый тип — `{enabled, z2mBaseTopics}`), нет `console.*`/`localStorage` в новых файлах (grep пуст), `support_package.py` — allow-list, не тронут, но уже исключает `zigbee_topology` по построению | unit (`zigbee-topology.test.mjs:33-44`) + backend regression `tests_backend/test_support_package.py` (`zigbee_topology not in plan["settings"]`, топик-строка не встречается в сырых байтах) + reviewer code audit (эта строка — часть требуемого доказательства AC17) | **Low-находка L1** по `diagnostics.py` — см. ниже, не про AC17 буквально (IEEE/raw topology туда физически попасть не может — эти данные никогда не пишутся в `settings`) |
| AC18 bounds/performance | доказан | `demo/benchmark_zigbee_topology.mjs`, лимиты `zigbee-topology.ts:63-66` (`TOPOLOGY_MAX_*`) | **лично прогнан** `npm run benchmark:zigbee-topology` — все 4 метрики в 5-13× запасе от бюджета | численный прогон — самодоказательство |
| AC19 lazy boundary | доказан | `zigbee-topology-overlay-bridge.ts:19` (`void import(...)`), `hp-zigbee-topology-settings.ts:89-91` (динамический импорт runtime) | `dist/houseplan-assets.json`: 3 новых chunk'а (`hp-zigbee-topology-overlay-*`, `zigbee-topology-*`, `zigbee-topology-runtime-*`) находятся только в `lazyFiles`, не в `initialViewFiles` | сверено вручную по сгенерированному манифесту; `initialViewGzipBytes` вырос всего на 490 B (294865→295355, см. заметку ниже) |
| AC20 i18n | доказан | `src/i18n/topology/{en,ru,de,fr}.json` | **лично прогнан** diff ключей — 27/27/27/27, `remote_count` не требует русской числовой формы (число не согласуется с существительным, стиль совпадает с прочими проектными ключами вида `"ещё {n}"`) | сравнение множеств ключей |
## Находки
### Medium (в скоупе, чинится в этой же задаче)
**M1 — невалидный ответ Zigbee2MQTT на верном топике зависает на полный 150-секундный таймаут и репортит `timeout` вместо `invalid_payload`.**
`src/zigbee-topology-runtime.ts:211-215`:
```ts
const value = parseMessage(message);
if (value && transactionOf(value) === transaction) responseResolve?.(value);
```
`parseMessage` (`:151-156`) при ошибке `JSON.parse` молча возвращает `null`.
Раз `value` ложно, ветка просто ничего не делает — `responseResolve` не
вызывается никогда для этого сообщения, и нет отдельной ветки, которая бы
пометила «получили мусор на верном топике с верной transaction» как
`invalid_payload`. Воспроизвёл лично (не поверил отчёту подагента на слово):
```
node --input-type=module -e "
import { refreshZ2mTopology, zigbeeTopologyRuntimeSnapshot } from './test-build/zigbee-topology-runtime.js';
... // подписка на bridge/response/networkmap отвечает 'not-json-garbage'
await refreshZ2mTopology(hass, 'zigbee2mqtt', 2000);
"
# elapsed ms 2006
# {"z2m:zigbee2mqtt":{"phase":"error","error":"timeout"}}
```
С реальным таймаутом 150 000 мс это значит: если Z2M/брокер вернёт битый
payload на правильный топик и с правильной transaction (повреждённая сеть,
несовместимая версия Z2M, прокси-мидлварь), пользователь увидит "Загрузка…"
**2.5 минуты**, а затем получит `error_timeout` («провайдер не ответил
вовремя») — хотя ответ пришёл и был получен, просто не распарсился. Это прямо
противоречит AC16 («invalid payload... дают локализованный status») в той
части, что статус обязан быть локализован *под причину*, а не просто не ронять
план: код `invalid_payload` в `ZigbeeTopologyErrorCode` существует специально
для этого случая, но недостижим с этого пути. Ни один существующий тест не
шлёт невалидный JSON на response-топик (`test/zigbee-topology.test.mjs`
проверяет только `retain`/foreign-transaction фильтрацию и «нет retained
info» — не «мусор в самом response»).
Правка в скоупе: в обработчике `bridge/response/networkmap` отличать «не наш
пакет» (нет transaction/чужая transaction — игнорировать и ждать дальше) от
«наш топик, наша transaction, но `parseMessage` вернул `null`» (сразу
`responseResolve`/reject с `invalid_payload`, не дожидаясь дедлайна).
### Low (сняты этим ревью с запиской, правка не обязательна)
**L1 — `custom_components/houseplan/diagnostics.py:41` отдаёт `config.get("settings", {})` без фильтрации**, то есть `zigbee_topology.enabled` и введённые `z2m_base_topics` (потенциально идентифицирующая MQTT-строка, например `private/site/zigbee2mqtt` — ровно такой пример использует тестовая фикстура #43/#54) попадают в стандартный HA "Download diagnostics" отчёт целиком. Это **не нарушает AC17 буквально**: AC17 и `docs/CONFIG-COMPATIBILITY.md` говорят про «IEEE/raw topology» и «provider snapshots», которые физически не персистятся и потому не могут попасть в diagnostics ни при каких условиях — а «diagnostics» отдельно от «support report» в тексте AC17 относится именно к этому неотфильтрованному core-механизму HA. `docs/SUPPORT-PRIVACY.md`, где явно упомянуты «base topics», описывает другую фичу — приватный support-relay (`support_package.py`), которая уже корректно фильтрует по allow-list и покрыта регрессионным тестом. Кроме того, поведение `diagnostics.py` (сырой дамп `settings` без редактирования, кроме `markers`) — не новое для #54: оно уже применялось ко всем существующим полям настроек до этой задачи. Снимаю без требования правки в рамках #54; стоит завести отдельным улучшением редактирования `diagnostics.py` по образцу `support_package.py`, если владелец сочтёт MQTT base-topic достаточно чувствительной строкой — это будет самостоятельная задача над всем diagnostics.py, а не точечная правка #54.
**L2 — `pushDirectionalLink` (`zigbee-topology.ts:114-126`) маркирует код `duplicate_link` как для байт-идентичных повторов, так и для конфликтующих значений одного направления** (сохраняется первое, второе отбрасывается с тем же кодом предупреждения). §9 ТЗ говорит про «exact duplicates», что подразумевает идентичность, а не любое повторное наблюдение. Функционально безопасно (не изобретает и не усредняет значения), просто имя предупреждения шире факта. Не требует правки — уточнение семантики кода предупреждения, а не дефект поведения.
**L3 — плотность диффа в нескольких местах указывает на автоматическую вставку без переноса строки** (`houseplan-card.ts`: `import ... './device-marker-geometry'; import { renderZigbeeTopologyOverlay } ...` на одной строке; аналогично в `houseplan-editor-runtime.ts` в нескольких местах, плюс однострочная вставка `<hp-zigbee-topology-settings ...>` в `_renderSettingsDialog`). Только стиль/читаемость, не влияет на поведение или типизацию (`tsc`/eslint не жалуются на длину строк в проекте). Снимаю без требования правки.
## Что проверено и корректно
- Полная цепочка defence-in-depth для admin-гейта (bridge-render, widget UI,
runtime `requireAdmin`) — три независимых точки отказа, лично воспроизвёл
снятие одной из них и убедился, что smoke ловит регресс.
- Инвариант «route не создаёт рёбер» (AC5) — лично воспроизвёл мутацию,
существующий unit-тест ловит регресс; ТЗ требовало именно mutation witness,
теперь он есть (пусть и не оформлен как постоянная запись в
`scripts/mutation-gate.mjs` — для чистого юнита текущее ревью засчитывает
это как достаточное доказательство).
- Приватность на уровне персистентной конфигурации (AC17 буквально): IEEE,
узлы, связи, ошибки провайдера физически не могут попасть в
`settings`/backup/export/support-report — они существуют только в
runtime-кэше (`WeakMap`), который никогда не сериализуется. Backend-тест
`tests_backend/test_support_package.py` подтверждает это для
support-пакета конкретным regression-кейсом.
- Ленивая загрузка (AC19): три новых chunk'а физически отсутствуют в
`initialViewFiles` манифеста; прирост initial-view бандла — 490 B (наличие
bridge-функции и её условного `import()`).
- Производительность (AC18): лично прогнанный бенчмарк на 500 узлах/2979
связях укладывается в бюджет с 5–13-кратным запасом по всем четырём
метрикам.
- i18n-паритет (AC20): 27/27/27/27 ключей en/ru/de/fr идентичны по составу;
видимый текст в UI-строках User Guide (RU) совпадает буквально с ключами
`toggle`/`zha_read`/`z2m_update` в `src/i18n/topology/ru.json`.
- Полный CI Validate на точном SHA `a2867df9`: типы/юниты/мутанты/бандл-синхрон,
все 3 браузерных smoke-шарда и perf-smoke — green; единственный красный job
(`golden`) — три ожидаемых, топологически объяснимых кадра общих настроек,
не принятых намеренно (правильное поведение по PROCESS.md).
- `settings.zigbee_topology` зарегистрирован в `scripts/config-field-registry.mjs`
и проходит `node scripts/config-audit.mjs` без ошибок; backend Python не
тронут ни одним файлом, что совпадает с явным заявлением ТЗ.
## Чего не проверял
- **Golden-изображения не пересматривал попиксельно.** Доверяю описанию
автора (3 кадра, все из экрана общих настроек, вертикальный сдвиг от новой
секции) и факту, что job `golden` в CI на этом SHA дал ожидаемый и
локализованный красный результат, а не разлетелся по всему матриксу.
Полный `golden:verify`/`golden:capture` локально не прогонял — это
предрелизный гейт (PROCESS.md §8), эталоны принимаются отдельно перед
бетой через `npm run golden:accept -- --reviewed` на Linux CI-артефакте.
- **Полный локальный прогон всех 221 браузерных смоков не делал** — CI
Validate на точном SHA уже прогнал все 3 шарда + сводный job зелёным;
дублирование того же прогона на той же ревизии ничего нового не покажет.
Лично перепрогнал только целевой `smoke_zigbee_topology_hover.mjs` и
дважды мутировал код (AC3, AC5), чтобы убедиться, что смок/юнит умеют
падать.
- **Клик/пан/зум-регрессия при видимом hover-слое** — не смокирована ни
автором, ни мной отдельным сценарием (заявлена в §14 ТЗ пунктом плана
автотестов, но `demo/smoke_zigbee_topology_hover.mjs` её не реализует).
Оценил как низкий риск по коду (`pointer-events:none` наследуется,
слой добавлен sibling'ом, не оборачивает markers) — доказательство
«чтением», не исполнением. Не поднимаю до Medium, так как это пробел в
тестовом покрытии одной из многих defence-in-depth гарантий, а не
найденный дефект поведения.
- **Немецкий/французский текст на предмет естественности формулировок** —
проверил только паритет ключей и отсутствие technical/raw-текста; носитель
языка эти переводы не вычитывал (как и остальные i18n-задачи проекта).
- **`custom_components/houseplan/diagnostics.py`** — прочитан и находка L1
зафиксирована, но не проверял остальные существующие поля `settings` на
предмет того, не более ли они уже чувствительны (вне скоупа #54).
- **Мультипровайдерный (ZHA+Z2M одновременно) сценарий на реальном браузере**
— логика дедупликации `resolveMappedTopologyHover` проверена юнитом и
рассуждением (Map/Set по `markerId` в общем цикле по топологиям), но нет
browser-smoke с двумя одновременно активными провайдерами.
## Вердикт
Один Medium-дефект в скоупе задачи (M1, Z2M invalid-payload на response-топике
маскируется под `timeout` на 150 секунд) — без High это жёлтый вердикт,
возврат автору на исправление в этом же issue (PROCESS.md §2.7, §4). Все 20 AC
прослеживаются к коду и тестам; 19 из 20 не имеют замечаний; missing route-
inference mutation witness для AC5 закрыт лично в ходе этого ревью и не
требует действий автора. Три Low-находки (L1–L3) сняты с запиской, правка не
обязательна.
Вердикт: жёлтый · заход r1 · блокирующих циклов 1/4 · High: 0 · Medium: 1 → в задаче
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/54-zigbee-topology-hover`, коммит `a2867df9a224` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `90d12b3a6980a4537b454e8a922212ac9ce0fed3`
```
git log --all --format='%H %T' | grep 90d12b3a6980
```