Files
houseplan-card/docs/reviews/CODE-REVIEW-588-r1.md
T
2026-09-18 11:44:41 +00:00

20 KiB
Raw Blame History

CODE-REVIEW-588-r1

Issue: #588 «Настройки устройства: отображение „Значение + статичный значок“ — значение видно, цвет маркера не меняется никогда». Материал: ветка issue/588-value-static-icon, вершина 9b0855b3c0aefde4b08ffef7a534d3f5a9320e05 (коммиты 99e1765d продукт + 9b0855b3 запись в scripts/smoke-links.mjs). Заход r1, блокирующих циклов израсходовано 0/4.

Скоуп диффа

src/logic.ts (DISPLAY_MODES, displayWantsValue/displayIsNeutral, normalizeDeviceDisplay), src/device-presentation-policy.ts, src/device-presentation.ts, src/device-pulse.ts, src/device-value-badge.ts, src/types.ts, src/houseplan-card.ts (ярлыки/подсказки + 3 ветки живого пылесоса), src/houseplan-editor-runtime.ts (4 ветки диалога устройства), custom_components/houseplan/validation.py, scripts/config-schema.json, i18n ru/en/fr/de, demo/golden/matrix.mjs, demo/smoke_static_icon.mjs (новые блоки), scripts/mutation-registry.mjs (+7 новых мутантов, 3 переехавших якоря), scripts/smoke-links.mjs, scripts/bundle-budget.mjs (перецентровка потолка), тесты (test/device-presentation-policy.test.mjs, test/device-presentation.test.mjs, test/device-pulse.test.mjs, test/logic.test.mjs, test/golden-matrix.test.mjs, tests_backend/test_validation.py, tests_backend/test_support_package.py), документация (USER-GUIDE.ru.md/.md, TESTING.md, CONFIG-COMPATIBILITY.md, CHANGELOG.md/.ru.md), плюс сгенерированные бандлы (класс D — законно вместе с исходником в одном коммите фичи).

Соответствует заявленному скоупу ТЗ; посторонних файлов класса A не задето.

Как проверялось

Дешёвые гейты подтверждены зелёным Validate на этом SHA (https://github.com/Matysh/houseplan-card/actions/runs/35336653719), поэтому tsc --noEmit/npm test/npm run build не перегонялись «для очистки совести» — но поскольку diff трогает геометрию/бэкенд-схему и рендер, ниже я всё равно исполнил протокольно важные гейты сам, чтобы не доверять только заявлению автора.

Гейт Команда Результат
typecheck + build npm run build зелёный, dist пересобран
bundle-sync npm run bundle:sync зелёный
unit-тесты (полный набор) node --test test/*.test.mjs 2765 pass, 0 fail, 1 skip — совпадает с заявленным автором результатом
no-new-any node scripts/no-new-any.mjs --base origin/dev --head HEAD зелёный, новых any нет (72 добавленные строки в 8 файлах)
smoke-select node scripts/smoke-select.mjs --base origin/dev --head HEAD 2 «зарегистрированные связи»: smoke_static_icon.mjs, smoke_device_preview_parity.mjs — оба прогнаны ниже
смок AC5–AC7 node demo/smoke_static_icon.mjs OK, все 28 фактов true (прогнан лично, не только по слову автора)
смок AC7 (парность превью) node demo/smoke_device_preview_parity.mjs OK, все факты true
бэкенд (чистый набор, без полного HA) pytest tests_backend/test_validation.py tests_backend/test_support_package.py (venv 3.12 + voluptuous, HA-тесты пропущены conftest'ом) 170 passed, 1 skipped — совпадает с заявленным
схема бэкенда python scripts/dump-config-schema.py --check manifest fresh: 291 paths — файл не изменился после генерации
мутанты (все 7 новых) node scripts/mutation-gate.mjs --id=<каждый> 7 из 7 «заявленный тест покраснел на мутанте»
мутанты (3 переехавших якоря) device-presentation-policy-static/-value/-diagnostics все 3 по-прежнему ловятся после переименования staticIcon→neutralFace
golden npm run golden:verify (Linux, локальный Chromium) 170 сцен побайтово совпали, ровно 2 «different»: device-icon-state-table-{light,dark} — визуально сверил diff/actual/baseline (см. ниже)
bundle-budget npm run bundle:budget 291346 B gzip — точно совпадает с заявленным фактом, потолок 292400±2000 не пробит
check-docs node scripts/check-docs.mjs ожидаемо ERROR: stale fingerprint — это предрелизный гейт (§8), не гейт ревью; пересъёмка идёт перед бетой

Golden — визуальная проверка. Открыл artifacts/golden/diff/device-icon-state-table-light.png: diff подсвечивает ровно один маркер (верхний левый, golden-light-one). baseline показывает его оранжевым цветным кружком (цвет RGB-лампы), actual — белым нейтральным кружком с текстом on. Это дословно то, что обещают К1/К2 и описание AC9 в хендоффе. Остальные 170 сцен совпали побайтово — это независимое подтверждение AC10 (я не поверил заявлению «остальное не сдвинулось», а прогнал полный golden:verify и увидел 0 неожиданных расхождений).

Не прогонял: полный HA-харнесс (test_ha_*.py — в песочнице нет homeassistant, пакет с пином 0.13.357 требует Python 3.14, которого в этом окружении нет; conftest.py корректно пропускает эти файлы без HA — совпадает с задокументированным поведением AGENTS.md); perf-профили (large-house-interaction-v1 формально применим, т.к. тронут houseplan-card.ts, но все три правки в нём — замена === 'static_icon' на displayIsNeutral(...) в трёх уже существующих ветках без новых вычислений и без новых проходов по данным; ТЗ прямо декларирует нулевую разницу в стоимости с value, и это подтверждается чтением диффа — новых сравнений по стоимости O(1) не добавляет); полную матрицу браузерных смоков (250 файлов) — smoke-select не назвал широких связей, задача не задевает ничего вне названных двух смоков.

Проверка AC по цепочке доказательств

AC Доказательство Проверено
AC1 test/device-presentation-policy.test.mjs — 5 статусов × value_static_icon, контраст с value тест прочитан, прогнан, ловит мутант value-static-icon-keeps-live-colour
AC2 там же + test/device-presentation.test.mjs (fallbackReason) — 4 причины × 2 режима, контраст со static_icon (не просит значения вовсе) прочитан, прогнан, ловит value-static-icon-loses-value-fallback
AC3 test/device-presentation.test.mjs — sourceDetails:false даёт 42 % для value_static_icon, пустоту для static_icon прочитан, прогнан, ловит value-static-icon-takes-the-sourceless-fast-path; дополнительно прочитан весь resolveDevicePresentation — все 4 вызывающих места (houseplan-card.ts×2, space-card.ts, space-render.ts) идут через единый путь, значит правка не точечная, а на всю подсистему
AC4 test/device-pulse.test.mjs, test/device-presentation.test.mjs (valueBadge===null, тревожный источник нейтрален) прочитан, прогнан, ловит value-static-icon-pulses-on-alarm
AC5 demo/smoke_static_icon.mjs — новый блок (valueStatic*, dynamicValueColoured) прогнан лично, 28/28 фактов true
AC6 тот же смок — puck/след на сопоставленной карте, .vacwarn на несопоставленной (m9), буфер _vacRt проверен в обеих ветках прогнан лично; отдельно перечитал src/vacuum-routes.ts:344-353 — routeWarningKey действительно null для ready, разведение по двум конфигурациям в r3 ТЗ оправдано и корректно реализовано; все 3 мутанта (-keeps-live-vacuum, -keeps-vacuum-overlay, -keeps-route-warning) прогнаны лично и ловятся
AC7 тот же смок + smoke_device_preview_parity.mjs прогнаны лично; плюс прочитан код 4 веток houseplan-editor-runtime.ts — все переведены на displayWantsValue/displayIsNeutral
AC8 test_validation.py, test_support_package.py, схема backend-тесты прогнаны лично (170 passed), dump-config-schema.py --check подтвердил свежесть; import_export.py не тронут — проверено чтением: create_export/build_space_merge прогоняют конфиг через тот же CONFIG_SCHEMA/MARKER_SCHEMA, значит новый токен проходит структурно, без специального кода — README ТЗ называет test_ha_import_export.py в плане тестов, но там нет отдельного теста на новый токен; это не пробел в защите (механизм общий и уже доказан через test_validation.py), а неточность плана автотестов — не блокирует
AC9 golden + test/golden-matrix.test.mjs golden визуально сверен (см. выше), контракт фикстуры прочитан и прогнан в общем наборе
AC10 golden:verify (170/172 идентичны), npm test (2765/2765) оба прогнаны лично
AC11 тест полноты локалей в общем наборе прогнан в составе npm test; плюс вручную сверил все 4 файла (ru/en/fr/de) — 3 ключа на каждый
AC12 USER-GUIDE.ru.md/.md, TESTING.md, CONFIG-COMPATIBILITY.md, changelog RU+EN прочитаны построчно, текст точно описывает К1/К2/К2а; см. находку ниже про одну неточную фразу в TESTING.md

Находки

Low — docs/TESTING.md называет непроверяемую поверхность «PDF»

Новая строка чек-листа (docs/TESTING.md:2652-2657) требует от ручного тестировщика убедиться, что value_static_icon показывает значение «включая... PDF, где источники резолвятся лениво». Формулировка дословно повторяет K3 из тела issue («…на всех поверхностях, включая основной путь рендера плана, карточку пространства и PDF»).

Фактически архитектурный PDF-экспорт (src/pdf/pdf-export.ts → pdf-scene.ts) не рисует маркеры устройств вообще — ни в каком режиме display, не только в новом. Это не регрессия задачи, а давнее и намеренное ограничение: комментарий src/pdf/pdf-scene.ts:393 — // Architectural export deliberately has no marker/device projection pass (существует с 2026-09-07, до #588), а функция rasters() в pdf-export.ts собирает только фон плана и декор-изображения, без единого прохода по DevItem/resolveDevicePresentation. Существующий (не менявшийся) докблок в начале src/device-presentation.ts тоже называет ровно три потребителя — «Interactive plan, static space card and the device editor preview» — PDF среди них нет.

Практический эффект: ручной тестировщик, дойдя до этого пункта, откроет PDF, не найдёт там ни одного маркера ни в старом, ни в новом режиме — и либо потратит время на выяснение, либо ошибочно заведёт баг. Функциональность задачи не страдает: ни один AC1–AC12 не требует поведения в PDF, а K3-формулировка описывает источник неточности (унаследована из тела issue, уже трижды прошедшего ревью ТЗ и не пойманная там).

Как обнаружено: не по совпадению имени — прочитал весь src/pdf/*.ts в поисках места, где resolveDevicePresentation/sourceDetails могли бы использоваться для PDF (раз K3 их называет), не нашёл ни одного вызова, и проверил историю строки-комментария, чтобы отличить «регрессию этой задачи» от «давнего факта архитектуры».

Не блокирует: узкая фраза в ручном чек-листе, не в AC, не в пользовательской документации (USER-GUIDE PDF не упоминает), не в коде. Достаточно снять слово «и PDF» из строки docs/TESTING.md:2652-2657 в этом же PR или следующей правкой; отдельного issue не требует. Снимаю без возврата на цикл — письменная фиксация здесь и есть запись по правилу Low (§2.7).

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

  • Два производных предиката (displayWantsValue, displayIsNeutral) полностью заменяют разбросанные сравнения с токеном; grep по всему src/**/*.ts на === 'static_icon' / === 'value' не нашёл ни одного пропущенного места — оставшиеся три вхождения оправданы (маппинг PresentationReason, быстрый путь К3, определение самого предиката).
  • Три ветки живого пылесоса (К2а) переведены на displayIsNeutral, режим подавляет puck/след/бейдж маршрута и не наполняет буфер позиций; разведение AC6 по двум конфигурациям (сопоставленная/несопоставленная карта) корректно устраняет тавтологию, найденную в ревью ТЗ r2 (M3) — перепроверено чтением vacuum-routes.ts независимо от заявления автора.
  • Мутационное покрытие полное и по-настоящему проверяет защиту: все 7 новых + 3 переехавших мутанта лично прогнаны и ловятся; ни один guard не «зелёный по умолчанию».
  • i18n (ru/en/fr/de) — 3 новых ключа на локаль, формулировка причины в предпросмотре («не меняет цвет маркера», а не «не меняет значок») отражает К1/К2 точно, а не скопирована бездумно со static_icon.
  • Golden-эталоны обновлены выборочно и обоснованно (RGB-лампа выбрана намеренно, чтобы кадр не был тавтологией) — визуально подтверждено.
  • Документация (USER-GUIDE ru/en, CONFIG-COMPATIBILITY, changelog) — точная, без лишних заявлений, кроме одной находки выше.
  • import_export.py/support_package.py не тронуты обоснованно: оба копируют display как непрозрачное значение, а не список констант — новый токен проходит структурно.
  • Перецентровка INITIAL_VIEW_GZIP_CEILING документирована с числами и не меняет общий бюджет; лично пересчитан факт (291346 B) — совпадает.
  • Трейлеры: Issue: #588 на обоих коммитах, User-Visible: yes на продуктовом коммите с правками в обоих changelog в том же коммите.

Чего не проверял и почему

  • Полный HA-харнесс (test_ha_*.py) — недоступен в песочнице (нет Python 3.14 / pytest-homeassistant-custom-component); это ожидаемое ограничение окружения, а не решение пропустить проверку — чистый backend-набор (170 тестов) прогнан лично и целиком покрывает AC8.
  • Perf-профили (large-house-interaction-v1) — не прогонял: правка в houseplan-card.ts — это замена трёх строковых сравнений на вызов функции той же асимптотики в уже существующих ветках, без новых проходов/аллокаций; ТЗ прямо декларирует нулевую разницу в стоимости, что подтверждается чтением. Несоразмерно гонять тяжёлый гейт ради трёх однострочных замен.
  • Полная матрица из 250 браузерных смоков — smoke-select не назвал широких совпадений сверх двух уже прогнанных, что при точечности диффа (одна подсистема представления устройства) достаточно.

Вердикт

Все 12 AC доказаны — частью автотестом с проверкой «умеет падать» (мутанты 1/1 по каждому защитному AC), частью личным запуском смоков/бэкенда/golden, не только на слово автора. Единственная находка — Low, вне AC, не блокирует.

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


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

  • Ветка: issue/588-value-static-icon, коммит 9b0855b3c0ae — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
  • Дерево материала: 477d28bbee006b670c293dfce422a1aa2ef1be6b
    git log --all --format='%H %T' | grep 477d28bbee00
    
  • Тело issue: 503d9f130d95a9bb723107478993f63ddb64e51e20b212e60a0f9d8dac331e44
  • Вердикт конвейера: green · High 0