Files
houseplan-card/docs/reviews/CODE-REVIEW-54-r2.md
T
2026-09-04 12:39:19 +00:00

21 KiB
Raw Blame History

CODE-REVIEW-54-r2 — #54: контекстные связи Zigbee на плане (hover overlay)

  • Issue: #54
  • Этап: code (PROCESS.md §2.7)
  • Диапазон общий: origin/dev...HEAD, origin/dev = ec9824f2, HEAD = f0ed526a (ветка issue/54-zigbee-topology-hover, детач HEAD, merge-base с origin/dev не сдвинулся — ec9824f2 тот же, что в r1, ребейза не было)
  • ТЗ: docs/specs/054-zigbee-topology-overlay.md, ревью ТЗ зелёное на заходе r2 — SPEC-REVIEW-54-r2.md
  • Заход: r2 код-ревью
  • Блокирующих циклов израсходовано: 1/4 (зелёных вердиктов ревью ТЗ бюджет не тратит, §2.10/#227; израсходован ровно жёлтый заход r1 код-ревью)
  • Вердикт: зелёный

Скоуп ревью (по дельте, PROCESS.md §2.10)

Предыдущий заход (r1) отревьюил материал на SHA a2867df9 полным разбором и вернул один Medium (M1). Дельта этого раунда — git diff a2867df9..HEAD, 3 коммита:

Коммит Класс Содержимое
aab5c3ca C (docs) публикация docs/reviews/CODE-REVIEW-54-r1.md шагом конвейера
4203a01b A/B (продукт+тест) правка M1: src/zigbee-topology-runtime.ts (+12/-3), новый юнит и мутант в test/zigbee-topology.test.mjs (+28), scripts/mutation-gate.mjs (+11, одна новая запись)
f0ed526a C/D (docs+generated) пересчёт sourceFingerprint в docs/images/screenshots.json после правки src/**; dist/** и custom_components/houseplan/frontend/** регенерированы пересборкой

Дельта локальна и не расширяет скоуп: новая подсистема не задета, контракт поведения docs/specs/054-zigbee-topology-overlay.md не менялся, ребейза на ушедший вперёд dev не было (merge-base с origin/dev — тот же ec9824f2, что и на r1). Объём разбора этого раунда — единственная правка (M1) плюс всё, до чего она дотягивается: контракт Z2M-runtime (AC13–AC16), i18n/perf-бюджет бандла, регенерация бандла/скриншотов. Остальные 17 AC (AC1–AC12, AC17–AC20) дельта не задевает — унаследованы из r1 без повторной проверки (раздел ниже).

Трейлеры всех трёх коммитов: Issue: #54 / User-Visible: no — верно, правка внутренняя (фича ещё не выпущена, публичного поведения не меняет).

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

Находка r1 Чем закрыта Где это видно
M1 — невалидный ответ Zigbee2MQTT на верном топике/transaction молча роняется (parseMessage → null), запрос висит полный 150 с таймаут и репортит timeout вместо invalid_payload (AC16) src/zigbee-topology-runtime.ts:201,219-222,227 — добавлен флаг responseActive, выставляемый в true только после публикации запроса (:227, сразу перед callService). Пока responseActive === false, «мусорное» сообщение на response-топике молча игнорируется (перед отправкой запроса это заведомо чужой/протухший пакет — ровно то, что и раньше корректно игнорировалось для retain===true). После responseActive === true тот же parseMessage → null немедленно вызывает responseReject?.(fail('invalid_payload')) (:220), и withTimeout (Promise.race) возвращает ошибку сразу, не дожидаясь дедлайна Новый юнит test/zigbee-topology.test.mjs:175-197 («Z2M runtime rejects a malformed response immediately instead of timing out») — воспроизвёл лично node --test --test-name-pattern="malformed response immediately" test/zigbee-topology.test.mjs: green, error: 'invalid_payload', cleanups: 2, elapsed < 250 ms при таймауте 500 ms. Мутант scripts/mutation-gate.mjs:5644-5654 (zigbee-topology-z2m-malformed-response-waits-for-timeout) снимает именно новую ветку (if (value === null) return; вместо ветки с responseReject) — лично прогнал node scripts/mutation-gate.mjs --id=zigbee-topology-z2m-malformed-response-waits-for-timeout: чистый прогон green, мутация красит тест, «поймано 1 из 1»
L1 (diagnostics.py не фильтрует settings) не тронута — принята r1 без правки, отдельным улучшением вне #54 не в скоупе этого раунда
L2 (duplicate_link шире факта) не тронута — принята r1 как снятая находка не в скоупе этого раунда
L3 (плотность диффа/стиль) не тронута — принята r1 как снятая находка не в скоупе этого раунда

Регрессии от фикса нет: существующий тест Z2M runtime verifies retained bridge info, correlates transaction and cleans subscriptions (test/zigbee-topology.test.mjs:138-172) теперь дополнительно шлёт stale-not-json на response-топик до responseActive (в момент подписки, до отправки запроса) — прогнан, остаётся green: подтверждает, что новая ветка не путает «протухший мусор до запроса» (игнорировать) с «мусор после запроса» (немедленный invalid_payload).

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

Документ docs/reviews/CODE-REVIEW-54-r1.md (материал: коммит a2867df9a224, дерево 90d12b3a6980a4537b454e8a922212ac9ce0fed3). Принято без повторной проверки в этом раунде, так как дельта их не задевает:

  • AC1–AC12, AC17–AC20 — доказаны в r1 таблицей AC (unit/smoke/mutation witness, чтение кода) и не связаны с изменённым кодом (refreshZ2mTopology затрагивает только AC13–AC16).
  • Defence-in-depth admin-гейта (AC3) — лично воспроизведённая r1 мутация (zigbee-topology-overlay-bridge.ts:17) не пересматривалась повторно.
  • AC5 «route не создаёт рёбер» — лично воспроизведённая r1 мутация normalizeZhaTopology не пересматривалась повторно, код zigbee-topology.ts дельтой не тронут.
  • Приватность на уровне персистентной конфигурации (AC17), включая backend-тест tests_backend/test_support_package.py — код settings-слоя дельтой не тронут, backend Python дельтой не тронут (git diff a2867df9..HEAD -- '*.py' пуст).
  • Golden red на трёх кадрах общих настроек (general-color-popover-desktop-en, settings-help-zoom-200-en-light, settings-help-zoom-200-ru-dark) — диагноз r1 (топологически ожидаемый сдвиг от нового блока Zigbee, не принят намеренно) не пересматривался: правка M1 не рендерит и не меняет разметку General Settings, только таймаут MQTT-обработчика. Остаётся предрелизным вопросом (PROCESS.md §8), не блокирует код-ревью.
  • Полный CI Validate на SHA a2867df9 (типы/юниты/мутанты/бандл-синхрон, 3 browser-smoke шарда, perf-smoke — success) — не перезапрашивался повторно; дельта покрыта локальными гейтами этого раунда (таблица ниже). На SHA f0ed526a зелёного Validate не найдено на момент ревью — гейты прогнаны лично.
  • AC18 (performance budget) — численный бенчмарк r1 (npm run benchmark:zigbee-topology, 500 узлов/2979 связей, 5–13× запас) не перезапускался: правка M1 не в горячем пути normalize/map/hover, а в MQTT-обработчике ошибок, который выполняется только при явном ручном refresh Z2M.
  • AC19 (lazy boundary) — три chunk'а вне initialViewFiles не перепроверялись поимённо; косвенно подтверждено этим раундом через npm run bundle:budget (см. таблицу гейтов) — initial View почти не изменился (295355 → 295365 B, +10 B), что согласуется с тем, что zigbee-topology-runtime.ts остаётся lazy-chunk'ом.

Как проверялось (гейты этого раунда)

npm ci не выполнялся — зависимости и Chromium уже установлены средой, рабочая копия чистая на входе и осталась чистой после всех прогонов.

Гейт Команда Результат
Типы npx tsc --noEmit green, код 0
Unit npm test 1906/1907 green, 1 skipped, 0 failed (включая новый тест zigbee-topology.test.mjs:175, subtest ok 1884)
Сборка npm run build green
Синхронность бандлов cmp dist/houseplan-card.js custom_components/houseplan/frontend/houseplan-card.js + npm run bundle:sync (нулевой diff после, git status --short пуст) идентичны байт-в-байт
Документация/скриншоты node scripts/check-docs.mjs (обязателен: дельта трогает src/**) green — Documentation checks passed (7 files, 12 external links); sourceFingerprint уже обновлён коммитом f0ed526a, imageSha256 кадров не изменился (диф docs/images/screenshots.json — только хеши источника)
Новый any node scripts/no-new-any.mjs --base a2867df9 --head HEAD green — 11 добавленных строк в 1 файле, новых any нет
Целевой мутант (новый, M1) node scripts/mutation-gate.mjs --id=zigbee-topology-z2m-malformed-response-waits-for-timeout green: чистый прогон green, мутация красит тест — «поймано 1 из 1»
Регрессия остальных zigbee-мутантов node scripts/mutation-gate.mjs --id=<каждый из 4 прежних id> (по одному) все 4 green, «поймано 1 из 1» каждый — правка M1 не сломала соседние guard'ы
Целевой smoke node demo/smoke_zigbee_topology_hover.mjs green — 12/12 полей true
Выбор смоков node scripts/smoke-select.mjs --base a2867df9 --head HEAD НЕОПРЕДЕЛЁННОСТЬ: 1 файл src/** изменён, 0 символов проекта на изменённых строках привязано к смокам — связь не доказана инструментом. Решение ревьюера: правка — таймер/promise-логика внутри refreshZ2mTopology, без DOM/UI поверхности; достаточным свидетелем считаю юнит + mutation witness выше и повторный прогон целевого smoke (регрессии на 12/12 нет)
Bundle budget npm run bundle:budget green — initial View 295365 B gzip (потолок 296000±2000, запас 4635 B); +10 B к r1 — предупреждение о низком запасе (#367) не новое и не относится к #54, project-wide долг
Модельные инварианты не прогонялся дельта не трогает геометрию/layout/marker.space/open_spans/рёбра комнат — гейт неприменим, инвариант из r1 не пересматривался
Backend pytest не прогонялся дельта не трогает custom_components/**/*.py (git diff a2867df9..HEAD -- '*.py' пуст)
Golden / полный smoke-матрикс (221) не прогонялся предрелизный гейт (PROCESS.md §8); диагноз r1 по трём кадрам General Settings не изменился, правка M1 не рендерит UI
CI Validate на точном SHA f0ed526a не найден зелёный прогон подтверждено оператором задачи в постановке ревью — прогон не найден/не завершён/не success; дешёвые гейты выше прогнаны лично взамен

Критерии приёмки — таблица (только AC, которых касается дельта)

AC Вердикт Доказательство Чем доказан Чем краснеет
AC14 Z2M contract доказан zigbee-topology-runtime.ts:201,216-224,227,234 — transaction/timeout/retained/foreign-фильтрация не изменились по сути, добавлена только явная ветка «наш топик, наша transaction-фаза, parseMessage вернул null» unit zigbee-topology.test.mjs:138-197 (обе Z2M-runtime теста) мутанты zigbee-topology-z2m-foreign-response-accepted, zigbee-topology-z2m-subscriptions-leak, zigbee-topology-z2m-malformed-response-waits-for-timeout — все три лично прогнаны, все красят соответствующий тест
AC15 no implicit work не тронут дельтой, унаследован из r1 — — —
AC16 failures доказан (M1 закрыта) zigbee-topology-runtime.ts:219-222 — invalid_payload теперь достижим немедленно на верном топике/фазе, без ожидания 150 с unit zigbee-topology.test.mjs:175-197 (error === 'invalid_payload', elapsed < 250ms при timeout 500ms) лично воспроизвёл мутацию zigbee-topology-z2m-malformed-response-waits-for-timeout — тест красится, поймано 1 из 1

Остальные AC (AC1–AC13, AC17–AC20) — вне зоны действия дельты, наследуются из r1 без повторной проверки (раздел «Унаследовано из r1»).

Что проверено и корректно

  • Правка M1 закрывает ровно то, что требовал r1: invalid_payload теперь достижим сразу после начала собственного запроса, а не только теоретически существует как код ошибки.
  • Различение «протухший/чужой пакет до запроса» (игнорировать) и «наш топик, наша фаза, но нераспарсиваемый payload» (немедленный invalid_payload) подтверждено двумя тестами одновременно: новым (мусор после запроса → ошибка) и существующим изменённым (мусор до запроса → игнорируется, ready всё равно достигается по корректному payload).
  • Cleanup обеих MQTT-подписок (cleanups === 2) сохраняется и на ошибочном пути — finally-блок (:238-242) не менялся.
  • Регенерация dist/**/custom_components/houseplan/frontend/** после правки синхронна во всех проверяемых копиях; docs/images/screenshots.json обновил только sourceFingerprint/sourceSha256, imageSha256 не изменился — правка не затронула видимый рендер, что снимает вопрос «одно число — один источник» для скриншотов в этом раунде (фикс не производит никакой пользовательской величины).
  • Ни одна из четырёх ранее существовавших zigbee-topology-* мутаций не перестала краситься — правка не задела соседние guard'ы контракта Z2M/ZHA.

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

  • Golden-изображения — не пересматривал; диагноз r1 (три ожидаемых кадра General Settings, топологически объяснимых новым блоком Zigbee) не переоценивался, поскольку правка этого раунда не касается разметки/стилей. Остаётся предрелизным гейтом.
  • Полный локальный прогон 221 браузерного смока — не делал; smoke-select на этой дельте вернул НЕОПРЕДЕЛЁННОСТЬ (0 символов проекта привязано к смокам на изменённой строке), решил не гонять полный матрикс: правка — чистая асинхронная логика внутри уже покрытого unit+mutation модуля, без новой DOM/UI поверхности; ограничился повторным прогоном целевого smoke_zigbee_topology_hover.mjs (green, без регрессии).
  • CI Validate на точном SHA f0ed526a — не нашёл зелёного прогона на момент ревью (подтверждено в постановке задачи); не проверял историю запусков через gh run list повторно после локальных гейтов.
  • Модельные инварианты и backend pytest — не прогонял: дельта не касается геометрии/layout и не касается custom_components/**/*.py, оба гейта неприменимы по построению диффа.
  • Немецкий/французский текст, мультипровайдерный browser-сценарий, diagnostics.py — как и в r1, не пересматривались: дельта их не касается.

Вердикт

Единственная находка предыдущего раунда (M1) закрыта точечной, доказанной правкой: новый юнит-тест воспроизводит ровно сценарий из находки и показывает исчезновение дефекта (invalid_payload вместо 150-секундного timeout), выделенный мутационный тест подтверждает, что защита реальна и умеет падать без неё. Регрессий у соседних guard'ов и AC, которых касается код Z2M-runtime, нет. Новых High/Medium находок в этом раунде не найдено.

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


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

  • Ветка: issue/54-zigbee-topology-hover, коммит f0ed526a5924 — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: f65587d326738cadfbbecd0d501fad0118bf10a2
    git log --all --format='%H %T' | grep f65587d32673