19 KiB
CODE-REVIEW-317-r1
- Issue: #317 — «Метрики под названием комнаты и тултип комнаты не показывают температуру и влажность, хотя датчики в комнате есть»
- Этап: code (S7-code-review)
- Заход: r1 · блокирующих циклов израсходовано 0 из 4
- Диапазон:
git log --oneline origin/dev..HEAD=f19ed10f(ТЗ),b3df25ab(SPEC-REVIEW, зелёный),6c9752f5(реализация, проверяемый SHA) - ТЗ:
docs/specs/317-room-climate-placement.md, SPEC-REVIEW-317-r1 — вердикт зелёный, High 0, Medium 0 - Ревьюер: Claude (сессия код-ревью), состязательно, без пояснений автора
Скоуп ревью
Один коммит реализации 6c9752f5, User-Visible: yes, оба changelog внутри
того же коммита. Диф: src/devices.ts (новый roomClimateMap/roomClimateKey,
areaClimateMap — тонкая совместимая обёртка), src/houseplan-card.ts
(_roomTemp/_roomHum читают room-aware key, внешний gate _renderRoomLabel
больше не блокирует area-less комнату), src/space-render.ts (hosted Static
получил тот же resolver вместо areaTemp), src/logic.ts/src/types.ts
(только комментарии, контракт room_id не меняется), test/devices.test.mjs
(+6 unit-тестов), demo/smoke_room_climate_placement.mjs (новый), правки
scripts/mutation-gate.mjs, scripts/smoke-links.mjs, docs
(ARCHITECTURE/CHANGELOG×2/TESTING/USER-GUIDE×2) и синхронный ребилд бандлов.
Первый код-ревью раунд — разбор полный, без разделов «Унаследовано»/«Закрытие раунда» (они появляются с r2).
Как проверялось
- Прочитаны
docs/SCOPE.md,AGENTS.md,PROCESS.md, тело issue #317 и все комментарии (gh issue view 317),docs/USER-GUIDE.ru.md,docs/ARCHITECTURE.md(раздел «Room climate is one pass…»). - Прочитан ТЗ
docs/specs/317-room-climate-placement.mdцеликом и SPEC-REVIEW-317-r1 (зелёный, Low-находки — терминология HA Area/HA-зона и формат доказательства AC8 — сняты автором ревью ТЗ, повторно не поднимаю). - Построчно сверен
git diff origin/dev...HEADс контрактом §6–§10 ТЗ:src/devices.ts:1386-1408(roomClimateKey,markerClimateTarget) —markerClimateTargetтребует именноmarker.area === nullвместе сspace+room_idдля local-room target — это тот же точный предикатmanualRoomWithoutArea, что уже используетresolveExplicitMarkerPlacement(src/devices.ts:1065-1067) для визуального размещения. Контракт не изобретён заново, а согласован с существующим прецедентом — соответствует заявлению ТЗ §6.2;src/devices.ts:1432-1463— приоритетentityTargets → deviceTargets → entity.area_id → device.area_idсовпадает с порядком §6.2 (1)-(4) построчно;src/devices.ts:1460— новый gateisRegistryEntryEnabled(уже существующий хелпер изha-binding-status.ts, переиспользован, не написан заново) отсекает HA-disabled entity/device — доказывает AC4;src/houseplan-card.ts:11956-11970(_roomTemp/_roomHumнаroomClimateKey(this._spaceModel()?.id, r)) и:12095(_renderRoomLabelвнешний gate теперьdisp.labelTemp || disp.labelHum || (disp.labelLqi && r.area) || disp.labelLight) — воспроизводит именно тот дефект, который описан в §3 ТЗ и в issue (внешний gate раньше блокировал area-less комнату независимо от включённыхdisp.labelTemp/Hum);src/space-render.ts:203-215— hosted Static построен на том жеroomClimateMap/roomClimateKey, а не на прежнем отдельномareaTemp(импортareaTempубран из файла) — единственный источник числа между View и hosted Static (AC8), а не два параллельных резолвера;src/space-render.ts:429-431— hosted Static не рисует metric-строки (rlm) вообще, только имя комнаты; humidity/tooltip там не появлялись и не появились — не расширяет незаявленный скоуп;docs/ARCHITECTURE.md,docs/USER-GUIDE(.ru).md,docs/TESTING.md, обаCHANGELOG— обновлены тем же коммитом, формулировки соответствуют §11 ТЗ и не вводят новую терминологию сверхdocs/USER-GUIDE.ru.md.
- Прогнаны дешёвые гейты и целевые smoke/mutation на SHA
6c9752f5(зелёного Validate на этом SHA нет — см. ниже). Полный список и результаты — в разделе «Гейты». - По каждому AC1–AC11 найдено прямое доказательство в коде/тесте (таблица ниже), а не принято на слово автора.
Гейты — что прогнал сам и с каким результатом
| Гейт | Команда | Результат |
|---|---|---|
| Typecheck | npx tsc --noEmit |
чисто, без вывода |
| Unit | npm test |
tests 1479 · pass 1478 · fail 0 · skipped 1 |
| Build | npm run build |
success, dist собран |
| Bundle sync | npm run bundle:sync |
пересборка не оставляет diff (git status --porcelain пуст после) — три копии (dist, custom_components/houseplan/frontend, demo/srv/assets) идентичны |
| Bundle budget | npm run bundle:budget |
initial View 256127 B gzip, запас 25873 B до бюджета 282000 B |
| Docs fingerprint | node scripts/check-docs.mjs |
Documentation checks passed (7 files, 10 external links) |
| any-gate | node scripts/no-new-any.mjs --base origin/dev --head HEAD |
«Новых any нет» (101 добавленная строка в 5 файлах) |
| Whitespace | git diff --check origin/dev...HEAD |
пусто |
| Smoke-select | node scripts/smoke-select.mjs --base origin/dev --head HEAD |
прямое совпадение: smoke_climate_once.mjs, smoke_climate_temp.mjs, smoke_cover_tap.mjs (по символу Marker), smoke_room_climate_placement.mjs |
| Smoke | node demo/smoke_room_climate_placement.mjs |
все 10 проверок true, OK |
| Smoke | node demo/smoke_climate_once.mjs |
OK (one-pass не растёт от room count — AC9) |
| Smoke | node demo/smoke_climate_temp.mjs |
OK, включая avgWithOption/avgStillCorrect |
| Smoke | node demo/smoke_cover_tap.mjs |
OK — совпадение по Marker оказалось шумом (в diff Marker тронут только doc-комментарием в types.ts), но раз инструмент назвал его прямым — прогнал |
| Mutation | node scripts/mutation-gate.mjs --id=room-climate-ignores-marker-placement |
тест покраснел, как обязан — smoke_room_climate_placement.mjs ловит мутанта, который игнорирует marker placement (AC10) |
| Visual | npm run golden:verify |
все сценарии passed, включая lighting-fill-temp-axis-split-dark, lighting-temp-glow-* (единственные golden-сцены с fillMode: temp) — нет визуальной регрессии на существующих HA-Area комнатах |
| Single-source | node --test test/single-source-numbers.test.mjs |
3/3 passed (механическая часть; смысловая часть — см. ниже) |
Не прогонял:
npm run invariants— diff не трогает рёбра комнат, записи толщины,layout,open_spans;marker.space/room_idздесь используются только как вход в climate-агрегатор (новая пара-ключ для карты температур), а не как геометрическая ссылка. Подсистема стен/толщины не затронута ни одной строкой diff — гейт неприменим, а не пропущен.python -m pytest tests_backend— diff не трогаетcustom_components/**/*.py(только пересобранные frontend-ассеты).- Полная матрица
demo/smoke_*.mjsи полныйgolden:capture/предрелизный набор — не требуются на этапе ревью (§8), выборsmoke-selectплюс явно названные в AC7/AC9 smoke уже покрывают три поверхности и hosted Static;golden:verifyпрогнал явно, так как diff меняет видимый рендер (temp-fill/labels area-less комнаты). - Performance-бенчмарки (
benchmark:*) — в AC не названы,smoke_climate_onceуже доказывает отсутствие роста числа проходов.
AC — доказательство
| AC | Требование | Доказательство | Статус |
|---|---|---|---|
| AC1 | Sensor из Area A с live marker в Area B голосует только в B, при совпадении — один раз | test/devices.test.mjs «explicit device placement moves one climate vote between HA areas» |
✅ прогнан, зелёный |
| AC2 | space + room_id + area:null даёт климат area-less комнате |
unit «a hidden real sensor supplies an area-less House Plan room» + smoke_room_climate_placement (localAutomatic, labelShowsAutomatic) |
✅ прогнаны |
| AC3 | Exact entity: приоритетнее parent device: только для этой entity |
unit «exact entity placement wins over its parent marker only for that entity» | ✅ прогнан |
| AC4 | Hidden участвует; removed/HA-disabled/unavailable — нет; tombstone #262 сохраняется | unit «removed and HA-disabled bindings cannot feed a local room» + hidden: true в двух других unit-тестах |
✅ прогнан |
| AC5 | use_climate_temp следует effective placement, air/non-air и округление не изменены |
unit «opted climate current_temperature follows manual placement» + существующие climate-тесты (npm test зелёный) |
✅ прогнан |
| AC6 | Explicit room source выше automatic, invalid не даёт silent fallback | sourceValue() не изменён diff'ом (проверено чтением — вызовы _roomTemp/_roomHum/roomTemperature идут в sourceValue первым тиром без изменений сигнатуры/тела) + smoke_room_climate_placement.explicitSourceWins |
✅ проверено чтением + smoke |
| AC7 | Label/tooltip/fill area-less комнаты — один result, перенос marker обновляет все | smoke_room_climate_placement: labelShowsAutomatic, tooltipSharesAutomatic, localTempFill, oldLabelDropsMovedValue, oldTempFillRemoved — все true |
✅ прогнан |
| AC8 | View/kiosk и hosted Static — одинаковый result, config не мутирует | код-ревью (не автотест, как и указала SPEC-REVIEW): space-render.ts использует тот же roomClimateMap/roomClimateKey, импорт areaTemp убран; smoke_room_climate_placement.staticLocalTempFill/staticOldFillRemoved подтверждают на практике; git diff не касается кода записи config |
✅ проверено чтением + smoke |
| AC9 | Один registry-проход на snapshot, не растёт с комнатами | demo/smoke_climate_once.mjs → OK |
✅ прогнан |
| AC10 | Мутант, игнорирующий marker placement, ловится тестом | mutation-gate.mjs --id=room-climate-ignores-marker-placement → «тест покраснел, как обязан» |
✅ прогнан |
| AC11 | typecheck/unit/build/bundle/docs/smokes зелёные | таблица «Гейты» выше | ✅ прогнан |
Находки
Blocking (High): нет.
В скоупе (Medium): нет.
Вне скоупа (Medium → issue): нет.
Low (снимаю с записью, без правки)
- Нет юнит-теста на дублирующиеся live-маркеры одного binding в
roomClimateMap. Комментарий в коде (src/devices.ts:1441-1442) заявляет «тот же детерминированный first-match contract, что иbuildDevices()», и реализация (!entityTargets.has(ref)) ... set(...)) действительно берёт первое вхождение по порядку массива — то же самое, чтоdeletePlanMarkerRecords/дедуп вbuildDevicesделают для дублей binding'ов в остальном проекте. Сценарий «два live-маркера с одинаковымbindingна разные комнаты» — уже известная малослучаемая аномалия данных (не новая, не введена этим diff), и mutation-guard #317 нацелен не на неё, а на сам факт учёта placement. Снимаю без правки: риск и вероятность малы, поведение читаемо и соответствует уже принятому в проекте паттерну; если владелец захочет явную регрессию на дубли — отдельная маленькая правка теста, не блокирующая эту задачу.
Что проверено и корректно
- Диагноз и весь контракт §6–§10 ТЗ воспроизведены в коде без расхождений;
markerClimateTargetиспользует тот же предикатarea === null+space+room_id, что и существующийresolveExplicitMarkerPlacement— не новый параллельный контракт для той же persisted-структуры. - Приоритет источников (marker exact → marker parent → registry entity → registry device → нет цели) реализован в заявленном порядке и покрыт unit + mutation.
- HA-disabled/removed/tombstone (#262) поведение не регрессировало:
переиспользован существующий
isRegistryEntryEnabledи неизменённыйremovedPlanBindings. - Единственность источника числа (AC7/AC8):
_roomTemp/_roomHum— общий путь для label/tooltip/fill в View; hosted Static куда раньше использовал другой резолвер (areaTemp) — теперь единыйroomClimateMap, импорт старого убран, что устраняет потенциальное расхождение, а не добавляет новое. golden:verifyподтверждает отсутствие визуальной регрессии на всех существующихfillMode: tempсценариях (HA-Area комнаты ведут себя как раньше — эффективный target совпадает с их текущимroom.area).- Persisted schema/model version не менялись (только doc-комментарии в
types.ts/logic.ts), миграции нет — совпадает с §9 ТЗ и явно провереноgit diffнаsrc/types.ts. - Трейлеры коммита
6c9752f5:Issue: #317,User-Visible: yes; оба changelog (docs/CHANGELOG.md,docs/CHANGELOG.ru.md) правлены в том же коммите с одной строкой на #317 — соответствует требованию. - Документация (ARCHITECTURE/USER-GUIDE ru+en/TESTING) обновлена по существу,
без новых незаявленных терминов;
docs/images/screenshots.jsonобновлён тем же коммитом,check-docs.mjsзелёный.
Чего не проверял
- Ручное тестирование на живом HA-инстансе не входит в цикл ревью (см.
инструкцию) — заменено чтением diff + browser smoke на реальном DOM карточки
(
demo/smoke_room_climate_placement.mjsреально монтируетhouseplan-card/houseplan-space-cardв браузере черезserve.mjs, не мокает). npm run invariantsи полныйdemo/smoke_*набор — обоснование в разделе «Гейты» выше.- Backend Python — не тронут.
- Поведение
sourceValue()изнутри — не переисследовал построчно, так как функция не изменена diff'ом; доверяю существующему покрытию (не входит в дельту этой задачи).
Вердикт
Реализация соответствует зафиксированному в ТЗ контракту, все AC1–AC11 подтверждены автотестом, mutation-guard'ом или чтением с явной пометкой, все дешёвые гейты и релевантные smoke/golden/mutation прогнаны мной лично и зелёные, trailers и changelog корректны. Blocking-находок нет, Medium нет ни в скоупе, ни вне скоупа. Одна Low-находка снята с записью (не блокирует).
Вердикт: зелёный · заход r1 · блокирующих циклов 0/4 · High: 0 · Medium: 0 · Документ: docs/reviews/CODE-REVIEW-317-r1.md