Files
houseplan-card/docs/reviews/CODE-REVIEW-126-r2.md
T
claude[bot] bc076d3f50
Проверка (CI) / Бэкенд: pytest в Home Assistant (push) Failing after 5m43s
Проверка (CI) / Предполётные проверки: документация, провенанс, процесс (push) Failing after 45s
Проверка (CI) / Классификация изменённых файлов (push) Successful in 21s
Проверка (CI) / Переиспользование: это дерево уже проверено (push) Successful in 1m16s
Проверка (CI) / HACS: валидация репозитория (push) Failing after 38s
Проверка (CI) / Hassfest: манифест интеграции (push) Failing after 43s
Проверка (CI) / Фронтенд: типы, юниты, мутанты, синхрон бандла (push) Failing after 6m40s
Проверка (CI) / Смоки в браузере (шард 1 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 2 из 3) (push) Skipped
Проверка (CI) / Смоки в браузере (шард 3 из 3) (push) Skipped
Проверка (CI) / Смоки: все шарды зелёные (push) Skipped
Проверка (CI) / Golden-кадры против принятых эталонов (push) Skipped
Проверка (CI) / Перф-смок: бюджет времени кадра (push) Skipped
docs: review document for #126
Issue: #126
User-Visible: no
2026-08-31 09:51:46 +00:00

17 KiB
Raw Blame History

CODE-REVIEW-126-r2

  • Issue: #126 — «Смена area устройства в HA не переносит иконку в новую комнату, если позиция была сохранена»
  • Этап: code (PROCESS.md §2.7)
  • Заход: r2 · блокирующих циклов 1/4
  • Диапазон: origin/dev...HEAD, HEAD = 26cb00db
  • Вердикт: зелёный · High: 0 · Medium: 0

Скоуп раунда

r1 (код-ревью, docs/reviews/CODE-REVIEW-126-r1.md, вердикт жёлтый на SHA, названном в вердикте как 34b99b95) нашёл один High и один Medium, оба в скоупе задачи. Ревью этого раунда — по дельте, а не заново, согласно PROCESS.md §2.9: причина не в ребейзе на ушедший вперёд dev (git merge-base HEAD origin/dev = 5c8cb58e = сам origin/dev, т.е. база не сдвигалась) и не в новой подсистеме — исправления r1 локальны к тестовой обвязке и одной строке продуктового кода.

SHA-несостыковка (зафиксирована, не блокирует). 34b99b95 из вердикта r1 не существует в текущей истории (git cat-file -t 34b99b95 → Not a valid object name). По временным меткам коммитов (633cb20e 09:24 → b319fc95 09:29 → 61dd7ccb 09:44, добавляющий сам CODE-REVIEW-126-r1.md) и по содержимому диффа b319fc95 — это тот же логический снимок, что был назван 34b99b95 в вердикте: тот же продуктовый коммит + тот же screenshot-refresh, непосредственно предшествующий добавлению документа ревью. Хэш разошёлся, видимо, из-за локального reword/amend перед публикацией. Использую b319fc95 как границу дельты — контент, а не сам хэш, подтверждён построчно ниже.

Дельта — коммиты d5268086 («fix: harden HA area relocation regressions», User-Visible: no) и 26cb00db («docs: refresh canonical screenshots for area relocation», User-Visible: no), диапазон b319fc95..HEAD:

demo/golden/harness.mjs          |  15 ++-
demo/golden/matrix.mjs           |  38 +++++++
demo/smoke_area_relocation.mjs   | 205 +++++++++++++++++++++++++++++++++++--
demo/smoke_discovery_filters.mjs |  15 +++
demo/smoke_marker_stay.mjs       |  10 +-
demo/smoke_subarea.mjs           |  11 +-
demo/smoke_vacuum_firstuse.mjs   |  21 ++--
demo/srv/demo.html               |   5 +
src/houseplan-card.ts            |   5 +
+ dist/**, custom_components/houseplan/frontend/** (сгенерированные копии, класс D)
+ docs/images/*.png, docs/images/screenshots.json (класс C, канонические скриншоты)

Продуктовая логика src/device-area-relocation.ts, src/types.ts, backend (validation.py, websocket_api.py, config-schema.json), unit-тесты резолвера — не изменились между b319fc95 и HEAD (git diff --stat по этим путям — пусто). По §2.9 п.4 повторно проверяю только те AC, чьё доказательство дельта задевает: AC-доказательства через browser smoke (AC2, AC6 §19 ТЗ) и Save-диалог устройства (AC-контракт roomTouched, r1 H1). Остальные AC наследую из r1 без повторной проверки — см. раздел ниже.

Закрытие раунда r1

Находка r1 Чем закрыта Где это видно
H1 (High) — demo/smoke_subarea.mjs и demo/smoke_marker_stay.mjs писали _markerDialog.room напрямую, минуя реальное DOM-событие #marker-room; после введения roomTouched эта запись стала no-op, smoke проходил vacuously (smoke_subarea реально падал 6/14 на 34b99b95) smoke_subarea.mjs теперь берёт реальный <select id="marker-room"> из renderRoot, ставит .value и диспатчит настоящий change-event (bubbles: true, composed: true), затем читает _markerDialog.roomTouched === true как отдельную проверку realRoomChangeMarkedTouched. smoke_marker_stay.mjs (тестирует другой инвариант — что позиция НЕ мигрирует при смене комнаты) явно проставляет roomTouched: true рядом с прямой записью room, восстанавливая запись как реальное действие, плюс добавлена проверка roomChangePersisted demo/smoke_subarea.mjs (diff), demo/smoke_marker_stay.mjs (diff). Прогнано мной: node demo/smoke_subarea.mjs → 15/15 true; node demo/smoke_marker_stay.mjs → 8/8 true (см. «Как проверялось»)
M1 (Medium) — demo/smoke_area_relocation.mjs покрывал 4 из 8 сценариев §19 ТЗ; cross-space, standalone entity marker, explicit-override/composite exclusion и cold-start backfill были доказаны только на уровне чистого resolver в unit-тестах, не через production bundle Добавлены сценарии crossSpaceRelocated (переезд между пространствами через тот же _syncAreaRelocations), standaloneEntityRelocated (entity-маркер без device), explicitOverrideExcluded (явный marker.room_id/area не трогается), compositeGroupExcluded (markerless light-group не переезжает и не получает attention), coldStartBackfilled (отдельная свежесозданная карта, первый authoritative rebuild, delete-before-config порядок) demo/smoke_area_relocation.mjs (diff, +205 строк). Прогнано мной: node demo/smoke_area_relocation.mjs → 18/18 true, все пять новых полей включены

Оба закрытия проверены исполнением теста на текущем HEAD, не пересказом коммита автора.

Унаследовано из r1

Без повторной проверки в этом раунде — дельта этих путей не касается:

  • Резолвер и его контракт (§8/§11/§12 ТЗ: duplicate-Area guard, known-binding area-changed, backfill по сохранённой точке, explicit wins, eligibility virtual/unverified/removed/composite) — src/device-area-relocation.ts, проверено в r1 построчно против ТЗ, unit-тесты test/device-area-relocation.test.mjs подтверждены как способные падать. Документ: docs/reviews/CODE-REVIEW-126-r1.md (раздел «Что проверено и корректно»), SHA b319fc95 (=34b99b95 вердикта).
  • AC3 (read-only/static truth) — space-render.ts/space-geometry.ts (markerPos, ignoreSaved), подтверждено smoke staticReadOnlyProjection/ staticMadeNoWrites в r1. Файлы не менялись в дельте.
  • AC9 (fail-safe, delete-first, retry) — _syncAreaRelocations (houseplan-card.ts:5137-5251 на момент r1), проверено по коду и smoke failedConfigRetryable/configRetrySucceeded. Единственная правка этой области в дельте — добавленная строка _saveConfigDebounced.cancel() внутри уже проверенного в r1 блока (см. «Новое в дельте» ниже), остальной контур не тронут.
  • AC10 (no Undo после relocation) — _cancelDeviceDrag()/ _devicePositionHistory.clear(), прочитано в r1, не покрыто отдельным smoke (низкий риск, отмечено в r1 как принятый пробел).
  • Backend схема и import/export (marker_area_snapshot, лимит MAX_KNOWN_DEVICES/MAX_MARKER_AREA_SNAPSHOT, tests_backend/test_validation.py, tests_backend/test_ha_import_export.py) — не менялись в дельте, приняты в r1 по коду и тестам.
  • Changelog/трейлеры продуктового коммита — 633cb20e (User-Visible: yes) уже содержит правки docs/CHANGELOG.md и docs/CHANGELOG.ru.md в том же коммите; это проверено в r1 и не изменилось.

Источник: docs/reviews/CODE-REVIEW-126-r1.md, диапазон на SHA 34b99b95 (содержимое = b319fc95 в текущей истории, см. «SHA-несостыковка» выше).

Новое в дельте (не из r1, самостоятельно проверено)

Автор указал ещё два самостоятельно найденных и исправленных дефекта, не поднятых r1. Проверены по коду и исполнением:

  1. Дублирующая config-запись при пересечении Area-sync с debounced пользовательской записью (src/houseplan-card.ts:5214, if (this._saveConfigDebounced.pending()) this._saveConfigDebounced.cancel();). Прочитано: _saveConfigDebounced (houseplan-card.ts:7695) при срабатывании таймера просто вызывает _writeConfig() над текущим this._serverCfg — не хранит отдельный payload. debounce().cancel() (houseplan-card.ts:756-760) очищает таймер без trailing-вызова. Поэтому отмена ожидающего таймера прямо перед синхронным await this._writeConfig() не теряет пользовательскую мутацию (она уже находится в this._serverCfg к моменту rebuild) и убирает гарантированный повторный сетевой write несколько сотен миллисекунд спустя. Корректно, находок нет.
  2. Golden-сценарии и два «legacy»-смока читали устаревший снэпшот cfg/ writes до one-time provenance-бэкафилла #126, из-за чего добавление резолвера начало превращать чужие тестовые фикстуры (намеренно расположенные вне «своей» HA-комнаты для проверки иконок на фиксированных координатах) в сценарии релокации и портить write-count инварианты. Правки изолируют это через markerAreaSnapshot-«прошивку» golden-фикстур (demo/golden/harness.mjs, demo/golden/matrix.mjs) и через ожидание _areaRelocationWrite/_writeChain перед снятием baseline в smoke_discovery_filters.mjs/smoke_vacuum_firstuse.mjs. Оба поля — реальные, существующие свойства (_areaRelocationWrite: houseplan-card.ts:1013, _writeChain: houseplan-card.ts:7489), не опечатки с молчаливым undefined. Подтверждено исполнением (см. ниже).

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

Дешёвые гейты. Инструкция ревью подтверждает Validate на 26cb00db — success (https://github.com/Matysh/houseplan-card/actions/runs/33368059072), это покрывает npx tsc --noEmit, npm test, npm run build на точном SHA; повторно не гонял. Дополнительно сам прогнал (дёшево, диф трогает src/**):

  • npm run build — green (несёт tsc --noEmit);
  • npm run bundle:sync — green; git status --porcelain после — пусто, т.е. dist/, custom_components/houseplan/frontend/, demo/srv/assets/ синхронны с committed-состоянием на этом SHA;
  • node scripts/check-docs.mjs — green (диф трогает src/houseplan-card.ts).

Smoke, отобранные по дельте и риску. node scripts/smoke-select.mjs --base b319fc95 --head HEAD:

Изменено файлов src/**: 1 · символов проекта на изменённых строках: 1
Прямое совпадение (9): _saveConfigDebounced →
  smoke_config_writer, smoke_danger_confirmation, smoke_decor_default_persist,
  smoke_discovery_filters, smoke_grid_snap, smoke_optional_space_model,
  smoke_save_race, smoke_v8_draft_write, smoke_vacuum_firstuse

Прогнал все 9 «прямых совпадений» плюс три smoke, изменённых в дельте напрямую (smoke_area_relocation, smoke_subarea, smoke_marker_stay — уже учтены/включены выше):

Smoke Причина прогона Результат
smoke_area_relocation.mjs изменён в дельте (M1 r1) 18/18 true
smoke_subarea.mjs изменён в дельте (H1 r1) 15/15 true
smoke_marker_stay.mjs изменён в дельте (H1 r1) 8/8 true
smoke_discovery_filters.mjs изменён в дельте + прямое совпадение OK
smoke_vacuum_firstuse.mjs изменён в дельте + прямое совпадение 27/27 true
smoke_config_writer.mjs прямое совпадение _saveConfigDebounced OK
smoke_danger_confirmation.mjs прямое совпадение OK (19/19 true; stderr-стек в выводе принадлежит намеренному error-path тесту markerCancelAccept, не сбою)
smoke_decor_default_persist.mjs прямое совпадение OK
smoke_grid_snap.mjs прямое совпадение 6/6 true
smoke_optional_space_model.mjs прямое совпадение 6/6 true
smoke_save_race.mjs прямое совпадение 4/4 true
smoke_v8_draft_write.mjs прямое совпадение 6/6 true

Итого 12/12 запущенных — зелёные. Остальные ~198 smoke не отбирались инструментом на этой дельте и не прогонялись — полная матрица является предрелизным гейтом, не гейтом ревью.

Golden. Дельта редактирует demo/golden/harness.mjs и demo/golden/matrix.mjs (новое поле markerAreaSnapshot в фикстурах) — это прямое изменение исходных данных для видимого рендера, поэтому прогнал npm run golden:verify целиком: 60/60 passed, 0 failed, включая два сценария, которые правка непосредственно защищает (orphan-references-*, где markerless-фикстуры намеренно вне «своей» комнаты).

Не прогонял: npm run invariants (дельта не трогает рёбра комнат, записи толщины, layout-схему или open_spans; src/space-geometry.ts не менялся между b319fc95 и HEAD); python -m pytest tests_backend (backend не менялся в дельте, r1 уже проверил точечно); performance-профили (не названы в AC, не задеты дельтой); полный smoke_* набор (~198 файлов, не отобраны инструментом и не требуются объёмом дельты).

Единый источник чисел

Дельта не вводит и не меняет ни одной пользовательски видимой величины (только тестовую обвязку и внутренний debounce-guard) — раздел «одно число — один источник» неприменим к этому раунду; актуален вывод r1 (не поднимал находок по этому классу).

Итог

Оба блокировавших находки r1 закрыты и подтверждены исполнением тестов, а не текстом коммита. Два дополнительных самостоятельно найденных дефекта (debounce double-write, golden/legacy-smoke изоляция от provenance-бэкафилла) читаются корректно и подтверждены прогоном golden (60/60) и целевых smoke (12/12). Новых находок нет. High: 0, Medium: 0 — задача готова к S8.