Files
houseplan-card/docs/reviews/CODE-REVIEW-274-r1.md
T
claude[bot] ad1ee9d13d
Validate / docs (push) Successful in 30s
Validate / process-workflow-sync (push) Successful in 29s
Validate / provenance (push) Successful in 38s
Validate / changes (push) Successful in 27s
Validate / process-gate (push) Failing after 33s
Validate / reuse (push) Successful in 50s
Validate / hacs (push) Failing after 16s
Validate / hassfest (push) Failing after 15s
Validate / backend (push) Failing after 9m9s
Validate / frontend (push) Successful in 9m54s
Validate / smoke (1) (push) Failing after 17m51s
Validate / smoke (3) (push) Failing after 18m23s
Validate / smoke (2) (push) Failing after 18m55s
Validate / smoke_done (push) Skipped
Validate / golden (push) Failing after 25m55s
Validate / performance_smoke (push) Failing after 50m54s
docs: review document for #274
Issue: #274
User-Visible: no
2026-08-23 19:45:24 +00:00

202 lines
21 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-274-r1
- Issue: [#274](https://github.com/Matysh/houseplan-card/issues/274) — беспроводной контроллер одинаково выглядит на плане и в preview
- Этап: code (PROCESS.md §2.7) · заход r1 · блокирующих циклов израсходовано 0 из 4
- Ветка: `issue/274-wireless-controller-parity`, коммит `c9b8497f58700748969daf9058304c774b2a7f2d`
- ТЗ: `docs/specs/274-wireless-controller-presentation-parity.md` (получило зелёный вердикт на spec-этапе, `docs/reviews/SPEC-REVIEW-274-r1.md`)
- Это первый code-review раунд задачи — разделы «Закрытие раунда r0» и «Унаследовано из r0» не применяются (правило PROCESS.md §2.9 касается r2+).
## Скоуп проверки
Диапазон: `git log --oneline origin/dev..HEAD` (3 коммита, из них 1 implementation-коммит
`c9b8497`) и `git diff origin/dev...HEAD`. Материал: `src/device-presentation.ts`,
`src/devices.ts`, `src/houseplan-card.ts`, новый unit-тест, новый browser smoke,
два новых мутанта в `scripts/mutation-gate.mjs`, документация (ARCHITECTURE, оба
USER-GUIDE, TESTING, оба CHANGELOG) и три копии бандла.
## Как проверялось
Проверка велась чтением кода **и** исполнением — не только чтением. Все команды
запускались непосредственно на `HEAD` (`c9b8497`).
1. **Прочитан диагноз в issue #274** (три комментария: аналитика, готовность ТЗ,
handoff на code review) и подтверждённая автором первопричина: `deviceFromMarkerDraft()`
строил draft только из одного маркера, поэтому tombstone отдельно удалённого
target-маркера не учитывался в preview так же, как в сохранённом плане; после
фильтрации цели `sources.sourceKind` у сохранённого маркера выпадал из `'controls'`,
и `controllerAvailability()` не вызывался — контроллер получал `unavail` при живых
battery/LQI/update.
2. **Прочитан фикс построчно**:
- `src/devices.ts:1287-1303` — `deviceFromMarkerDraft()` принимает опциональный
`siblingMarkers`, строит `markers = [...siblingMarkers.filter(id≠marker.id), marker]`
и вызывает `buildDevices()` с полным ростером — draft теперь проходит те же
tombstone/ownership фильтры, что и сохранённый план.
- `src/houseplan-card.ts:19902-19911` — вызов `deviceFromMarkerDraft()` в
`_markerPreviewDevice()` получил `siblingMarkers: this._markers` (тот же источник,
из которого строится сам план, см. `src/houseplan-card.ts:4533`), а memo-ключ
превью включил `contentFingerprint(this._markers)`, чтобы инвалидироваться при
изменении общего ростера, а не только текущего маркера.
- `src/device-presentation.ts:624-640` — введён `configuredController` (маркер не
manual-virtual-light и имеет непустой persisted `controls`) и `controllerFace`
(сохраняет прежнее условие `sourceKind === 'controls'`, добавляя случай, когда
весь runtime-граф цели вычищен tombstone, но маркер по конфигурации остаётся
контроллером). `controllerAvailability()` не изменена — это соответствует
явному запрету ТЗ (§9 п.3) менять её без отдельного красного unit.
3. **Проверено, что новый unit умеет падать** (дисциплина «тест должен уметь падать»)
отдельно для каждого из двух механизмов:
- временно вернул `src/device-presentation.ts` к версии `origin/dev` (без
`configuredController`/`controllerFace`) → `node --test test/device-presentation.test.mjs`
— новый тест `issue 274 keeps a wireless controller…` красный:
`availability: 'unavailable'` вместо `'available'`;
- временно вернул `deviceFromMarkerDraft()` к однострочной версии (`markers: [marker]`,
без sibling-роутера) → тот же тест красный: `draft honours the same tombstones as
the plan` — `controls` у draft остаётся `['light.wall_group']` вместо `[]`.
- в обоих случаях восстановил файл до состояния `HEAD` и перепроверил зелёный прогон.
4. **Прогнаны оба новых мутанта из `scripts/mutation-gate.mjs` напрямую**
(`node scripts/mutation-gate.mjs --id=<id>`), а также существующий мутант
`controller-availability-follows-target`, чей `find/replace` был обновлён в этом
diff из-за переименования переменной — все три «покраснели, как обязаны», без
ложных срабатываний в чистом прогоне.
5. **Прогнаны все дешёвые гейты заново на `HEAD`**: `npx tsc --noEmit` — чисто;
`npm test` — 1167/1167 (0 fail, 0 skip — расходится с текстом автора «1166 pass,
1 skip», см. «Чего не проверял»); `npm run build` — чисто, три копии бандла
(`dist/houseplan-card.js`, `custom_components/.../houseplan-card.js`, синхронизированный
`demo/srv/assets/houseplan-card.js`) идентичны байт-в-байт; `node scripts/check-docs.mjs`
— «Documentation checks passed (7 files, 10 external links)».
6. **`node scripts/smoke-select.mjs --base origin/dev --head HEAD`** дал ровно тот же
набор из пяти смоков, что назвал автор (`smoke_wireless_controller_parity`,
`smoke_binding_picker`, `smoke_climate_once`, `smoke_controls`, `smoke_cover_tap`,
все — «прямое совпадение»). Все пять исполнены (после `node scripts/bundle-sync.mjs`,
без которого `demo/srv/assets/houseplan-card.js` не существовал в этом checkout) —
все green, включая новый смоук с 11 отдельными assertion (`previewMatchesPlan`,
`planKeepsLiveLqi`, `activeTargetWorksOnBothSurfaces` и др.).
7. **Прочитан новый смоук `demo/smoke_wireless_controller_parity.mjs` целиком** —
сценарий воспроизводит ровно field fixture из ТЗ (event=unknown, battery=100,
LQI=164, update=off, group=off), сравнивает `_devicePresentation()` плана с DOM
(`data-state`, класс `unavail`, текст LQI) и с `hp-device-preview` в открытом
диалоге того же маркера, затем возвращает target активным и проверяет, что обе
поверхности синхронно становятся `working`. Работает через production-бандл
(`assertFreshDemoBundleUnlessAllowed`), а не через мок.
8. **Проверено «одно число — один источник»**: `lqiText` вычисляется один раз внутри
`resolveDevicePresentation` (`src/device-presentation.ts:764`) и потребляется и
планом, и preview из одного и того же объекта `ResolvedDevicePresentation` — новый
код не вводит второй путь для LQI. `test/single-source-numbers.test.mjs` прогнан
отдельно — зелёный.
9. **Проверено, что фикс не ослабляет негативный контракт #251**: полный прогон
существующего теста `issue 251 separates controller availability from controlled
target status` (virtual, event-only-unavailable, все-diagnostics-unavailable,
alarm-приоритет) остался зелёным без изменений тела теста; отдельно прослежено,
что во всех его сценариях `light.wall_group` остаётся persisted-control с реальным
состоянием (не tombstoned), поэтому `sources.sourceKind` там не покидает `'controls'`
и новая ветка `configuredController` в этом тесте не участвует — расширение
покрытия для tombstone-случая сделано отдельным новым тестом (issue 274), что
допустимо согласно техническому предположению №4 ТЗ (раскладка тестов — решение
ревьюера при сохранении AC2/AC4 и исполняемых мутантов).
10. Проверено, что diff не трогает геометрию/рёбра/`layout`/`marker.space`/`open_spans`
→ `npm run invariants` не требуется (см. «Чего не проверял»).
11. Проверены трейлеры коммита `c9b8497`: `Issue: #274`, `User-Visible: yes`; в том
же коммите обновлены `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` (проверено
`git show c9b8497 --stat`).
## AC — построчная проверка
| AC | Статус | Доказательство |
|---|---|---|
| AC1 (fixture доступна и нейтральна) | Выполнен | новый unit в `test/device-presentation.test.mjs` через production `buildDevices()`; `saved.entities === own`, `visual.availability==='available'`, `lqiText==='164'` — проверено исполнением |
| AC2 (plan и preview совпадают) | Выполнен | `smoke_wireless_controller_parity.mjs` (`previewMatchesPlan`, `planDomAgrees`) — исполнено против production-бандла |
| AC3 (группа меняет только status) | Выполнен | тот же смоук: `off→on` переводит обе поверхности в `working`, `availability` не меняется (`activeTargetWorksOnBothSurfaces`) |
| AC4 (registry/state generation) | Выполнен частично, по существу | смоук воспроизводит начальный event-only-плюс-диагностика ростер, `_maybeRebuildDevices()` (registry-путь), state-тик цели (off→on) и сверяет обе поверхности после каждого шага; отдельного сценария continuity hold/candidate/commit не добавлено — принято как обоснованное сужение, см. ниже |
| AC5 (негативный контракт #251 не ослаблен) | Выполнен | существующий тест #251 не тронут и зелёный; прочитано, почему новая ветка `configuredController` не активируется в его сценариях |
| AC6 (Static/kiosk/theme parity) | Выполнен по коду, не по golden | фикс расположен в `resolveDevicePresentation()`, общем для View/kiosk/Static/preview — все потребители получают один расчёт; `npm run golden:verify` не прогонялся полностью, см. «Чего не проверял» |
| AC7 (два исполняемых мутанта) | Выполнен | `wireless-controller-loses-filtered-target-role` и `wireless-controller-preview-drops-sibling-markers` — оба прогнаны напрямую, оба «покраснели, как обязаны» |
| AC8 (локальные гейты) | Выполнен | `tsc --noEmit`, `npm test`, `npm run build`+bundle parity, `check-docs.mjs` — прогнаны заново на `HEAD`, см. выше |
## Находки
Блокирующих (High/Medium в скоупе) находок нет.
**Low — принято без изменений, с записью.**
1. `src/houseplan-card.ts:19902` — memo-ключ превью теперь считает
`contentFingerprint(this._markers)` при каждом вызове `_markerPreviewDevice()`,
т.е. на каждый ре-рендер открытого диалога маркера, а не только на его открытие.
Это полный обход всего персистентного ростера маркеров (до ~200 по цели J6), а не
точечный lookup. Снято как некритичное: операция ограничена editor-only
поверхностью (диалог маркера, desktop admin), стоимость — линейный проход по
строковому представлению конфигурации без сети/polling, что укладывается в
допущение ТЗ §10 «bounded O(e) fingerprint/lookup on device rebuild or snapshot
capture»; аналогичный по стоимости `contentFingerprint` уже используется в этом
файле для `_serverCfg`/`_layout`. Замер: не проводился (не назван ни в AC, ни как
perf-sensitive путь), поэтому вывод сделан по анализу сложности, а не по профилю.
2. Автор заявил в handoff-комментарии «`npm test` — 1167 tests: 1166 pass, 1 skip,
0 fail»; мой прогон на этом же `HEAD` даёт 1167 pass, 0 skip, 0 fail. Расхождение
не влияет на вывод (0 fail в обоих случаях) и объясняется тремя условными
`t.skip(...)` в `test/process-gate.test.mjs`, зависящими от доступности `git`/
платформы — в среде ревью `git` доступен, поэтому skip не сработал. Не находка,
зафиксировано для прозрачности числа, которое звучит в отчёте автора.
## Что проверено и корректно
- Корневая причина (roster parity + fallback availability) подтверждена чтением
кода и **исполнением**: оба ключевых механизма фикса умеют ломаться (см. п.3
«Как проверялось») и оба перехвачены именно тем unit-тестом и именно тем
мутантом, которые заявлены в AC7.
- `controllerAvailability()` не изменена — запрет ТЗ соблюдён; фикс расположен
строго на границе roster/sourceKind, как и требовало предположение №1 ТЗ.
- Manual-virtual-light и cover-face исключены из `configuredController` явно
(`!isManualVirtualLightMarker(...)`, `sourceKind !== 'light' && !== 'cover'`) —
дедициованная availability этих лиц не затронута; негативный контракт #251
остаётся зелёным без правок его теста.
- Единственная точка вызова `deviceFromMarkerDraft()` в продакшен-коде обновлена;
сигнатура обратно совместима (`siblingMarkers` — опциональный параметр).
- Документация (ARCHITECTURE.md, оба USER-GUIDE, TESTING.md, оба CHANGELOG) и три
копии бандла обновлены в том самом implementation-коммите с верными трейлерами.
- `smoke-select.mjs` вернул именно тот набор, что заявлен автором; все пять смоков
исполнены и зелёные на production-бандле.
- «Одно число — один источник» для LQI не нарушено: единственный расчёт
`lqiText` внутри `ResolvedDevicePresentation`.
## Чего не проверял (и почему)
- **`npm run golden:verify` (полная матрица).** Попытка прогона в этой среде не
успела завершиться за 180 c (лимит фонового шага) и не выдала промежуточного
вывода — полный набор захватывает десятки визуальных фикстур в headless Chromium
и по ТЗ (§ «Локальные гейты реализации», AC8) относится к prerelease-гейтам, а
не к гейту ревью. Дополнительно проверено по коду: существующая golden-фикстура
`device-icon-state-table-{light,dark}` (`demo/golden/matrix.mjs:404-424`) включает
контроллер с `controls` (`golden-left-linkquality`), но не воспроизводит именно
tombstone-сценарий из #274 — то есть полный прогон дал бы только общую
регрессионную проверку («ничего другого не сдвинулось»), а не прямое
доказательство AC6, которое уже даёт `smoke_wireless_controller_parity.mjs`
прямым сравнением DOM плана и preview. Решение: не прогонять полную матрицу на
этом раунде, отметить как открытый пункт для prerelease-гейта.
- **`npm run invariants`.** Diff не трогает рёбра комнат, `layout`, `marker.space`,
`open_spans` или другую геометрию — гейт неприменим к этой задаче.
- **`python -m pytest tests_backend`.** `custom_components/**/*.py` не тронут.
- **Continuity hold/candidate/commit сценарий из AC4 отдельным unit'ом.**
Подтверждённая автором первопричина — roster/tombstone на уровне `buildDevices()`,
а не гонка поколений `RenderDeviceSnapshot`; специализированного теста на
hold/candidate/commit для именно этого маркера не добавлено. Принято как
обоснованное сужение (ТЗ, техническое предположение №4: раскладка lifecycle-тестов
— решение ревьюера при сохранении AC2/AC4 и исполняемых мутантов) — существующие
тесты continuity/снапшота (не входящие в этот diff) прошли без изменений в общем
прогоне `npm test`, то есть регрессии в этой части не внесено, но новый прямой
сценарий для #274 не добавлен.
- **Полный browser smoke-набор (`ls demo/smoke_*.mjs`, 175 файлов).** Выбор
`smoke-select.mjs` показал только «прямые совпадения», без «неопределённости» —
расширять выборку не потребовалось.
- **Performance-профили.** Не названы в AC и не затронуты чувствительным к перфу
путём напрямую (только Low-находка №1 про memo-ключ, разобранная по сложности).
- **Ручное тестирование в браузере с реальным HA.** Не входит в цикл ревью;
замещено production-bundle browser smoke и unit-тестами, исполненными лично.
## Итог
AC1–AC3, AC5, AC7, AC8 доказаны исполнением. AC4 и AC6 доказаны по существу
(код общий для всех поверхностей, ключевой сценарий смоука зелёный), но не
полным набором continuity- и golden-проверок — оба сужения объяснены выше и не
меняют вывод: корневая причина устранена в правильном месте, оба обязательных
мутанта ловятся, негативный контракт #251 не ослаблен, документация и трейлеры
в порядке. Находок уровня High/Medium нет.