20 KiB
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— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
477d28bbee006b670c293dfce422a1aa2ef1be6bgit log --all --format='%H %T' | grep 477d28bbee00 - Тело issue:
503d9f130d95a9bb723107478993f63ddb64e51e20b212e60a0f9d8dac331e44 - Вердикт конвейера:
green· High 0