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

196 lines
20 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-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**
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/588-value-static-icon`, коммит `9b0855b3c0ae` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `477d28bbee006b670c293dfce422a1aa2ef1be6b`
```
git log --all --format='%H %T' | grep 477d28bbee00
```
- Тело issue: `503d9f130d95a9bb723107478993f63ddb64e51e20b212e60a0f9d8dac331e44`
- Вердикт конвейера: `green` · High 0