19 KiB
SPEC-REVIEW-655-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/655
- Этап:
S4-spec-review(ревью ТЗ, PROCESS.md §2.4) - Трек: лёгкий (
small), автор явно называет критерий: одна backend-поверхность жизненного цикла, сложность 3/10, без миграции, нового UX-контракта, touch/perf влияния. - Материал: тело issue #655 целиком (разделы «Проблема», «Ожидается», «Не
скоуп», «Аналитика»,
## ТЗ) + два комментария владельца (аналитика/оценка, передача на ревью) + два служебных комментария о падении автоматического прогонаmodel review(полезны только как контекст: метка не менялась, до этого раунда задача реального ревью не проходила). - Заход: r1 · блокирующих циклов израсходовано 0 из 2
- Роль: ревьюер ТЗ (не автор)
Скоуп ревью
Backend-регрессия долговечности: VirtualLightController (0,5 с коалесценция)
и TrailRecorder (10 с debounce) гарантированно сбрасывают отложенную запись
при unload/reload интеграции, но не при штатной остановке/перезапуске HA
(EVENT_HOMEASSISTANT_STOP/entry.async_shutdown, не async_unload_entry).
Задача — добавить единый идемпотентный shutdown-путь для обоих хранилищ и
перевести фоновую задачу виртуальных ламп с голого asyncio.create_task на
HA-tracked API. Не в скоупе: троттлинг toggle, ACL system-read-only,
интервалы/формат store/трейлов, WebSocket API.
SCOPE-проверка (docs/SCOPE.md): владелец относит задачу к J1 (живой
статус на плане должен отражать последнее принятое состояние) и J6
(«keep the plan true as the home evolves» — состояние обязано пережить
штатное обслуживание HA). Обе фичи (virtual light toggle, trail book) уже
приняты в продукт раньше этой задачи; здесь чинится не расширение
функциональности, а её надёжность — новой scope-дыры это не открывает и
существующий периметр не расширяет.
Как проверялось
- Прочитаны
docs/SCOPE.md,docs/process/REVIEWER.md(по ссылкам — PROCESS.md §2.4, §7.1, §7.2, §4; §2.10 не применялся — это r1),AGENTS.md. - Прочитано тело issue #655 целиком (
gh issue view 655 --json body,comments, labels) — все четыре комментария владельца/пайплайна, вопросов владельцу в тексте нет, подтверждаю. - Сверены обязательные разделы §7.1:
- сценарий + «что человек увидит» — присутствуют одной фразой в разделе
«Ожидается» (это часть тела issue, не только
## ТЗ; материал — тело issue целиком): «домочадец переключил виртуальную лампу… администратор перезапустил HA… после старта лампа в том состоянии, в каком её оставили» — без терминов реализации, названы обе релевантные персоны (docs/SCOPE.md: household members, home admin). - проблема — описана дважды (верхний уровень +
### Проблемавнутри## ТЗ), согласованно, с точными путями/строками кода. - скоуп/не-скоуп — явно, дважды (верхний уровень кратко,
### Вне скоупаподробно), без противоречий между версиями. - контракт поведения — 5 пронумерованных пунктов, каждый проверяем независимо (см. п.4 ниже — перепроверены по коду).
- UX — отдельного заголовка нет, но это оправдано: правка не трогает ни одного видимого интерактивного элемента, весь наблюдаемый эффект уже покрыт сценарием «что человек увидит» (состояние после restart). Нет видимой поверхности, которую полноценный раздел UX мог бы описать сверх этого.
- модель данных и миграция — явное «нет»: контракт п.5 «Никакой миграции данных или новой настройки нет», формат store/трейлов не меняется (контракт п.4).
- i18n — не упомянут явно, но обоснованно: изменение не добавляет ни
одной пользовательской строки (только серверный
_LOGGER), других backend-задач в проекте i18n тоже не касается. - план автотестов — распределён по AC1–AC5, каждый называет метод доказательства (HA-harness/unit/mutant) — соответствует требованию «указание способа доказательства» для каждого AC.
- риски и откат —
### Риски и откат, названа единственная реальная угроза (двойной cleanup / гонка timer-flush) и как она закрыта (идемпотентность + AC3). - release-артефакты —
User-Visible: yes, оба changelog в том же коммите, generated bundle не меняется — явно.
- сценарий + «что человек увидит» — присутствуют одной фразой в разделе
«Ожидается» (это часть тела issue, не только
- Перепроверены фактические утверждения ТЗ по коду на SHA
9a7020ce(== origin/dev), а не приняты на слово:custom_components/houseplan/virtual_lights.py:128-130(_schedule_save) — буквальноasyncio.create_task(self._delayed_save()), не HA-tracked API. Подтверждено дословно как в issue.custom_components/houseplan/__init__.py:258-275(async_unload_entry) — вызываетrec.async_teardown()иvirtual_lights.async_flush(), но других обработчиковEVENT_HOMEASSISTANT_STOPне регистрирует.grep -rn "EVENT_HOMEASSISTANT_STOP" custom_components/houseplan/— 0 вхождений во всём пакете (толькоradar.py:117слушаетEVENT_CORE_CONFIG_UPDATE, к делу не относится). Заявление «слушателя нет» подтверждено, не принято на веру.custom_components/houseplan/trails.py:472-478(async_teardown) — подself._refresh_lockвызывает_close_subscriptions()и, если была отложенная запись (pending),await self.store.async_save(self.book.data)— ровно то поведение, которое ТЗ называет «уже безопасным при reload/unload».tests_backend/test_ha_virtual_lights.py:114—test_unload_flushes_a_toggle_still_inside_the_debounce_windowсуществует и называется как в issue; тестhass.async_stop()-сценария в проекте не найден (grep -rn "async_stop" tests_backend/— 0 совпадений) — подтверждает пробел в покрытии, который и закрывают AC1/AC2.custom_components/houseplan/manifest.json:22—single_config_entry: true. Это устраняет риск, который я сам держал в уме при чтении контракта п.1–2 (совместное состояниеTrailRecorder, ключhass.data[DOMAIN]["trail_recorder"], не per-entry): при ровно одном возможном entry «один привязанный к entry обработчик» не может коллизировать с другим экземпляром интеграции.hass.async_create_task/entry.async_create_background_taskуже используются в проекте (radar.py:115,frontend_registration.py:561,websocket_api.pyв нескольких местах) — контракт п.3 не изобретает новый паттерн, а переиспользует существующий, что снижает технический риск реализации.VirtualLightController.__init__(virtual_lights.py:79-86) не хранитhass/entry— реализации потребуется протянуть ссылку наhassв конструктор или передавать её в_schedule_save. Это чисто техническая деталь (модуль/интерфейс класса — то, что ТЗ прямо отдаёт исполнителю), не продуктовый вопрос и не блокер: конструктор вызывается один раз (store.py:136) в контексте, гдеhassдоступен.
- Ветки/коммитов для #655 не существует (
git log --all --oneline | grep 655— пусто;git diff origin/dev...HEADпуст,HEAD == origin/dev == 9a7020ce). Гейты (tsc,test,build, смоки,golden,pytest tests_backend, инварианты) неприменимы на этапеspec— продуктового кода ещё нет, это штатное состояние, а не пропущенный гейт. - HA-рантайм-утверждение («на
EVENT_HOMEASSISTANT_STOPHA зовётentry.async_shutdown, неasync_unload_entry») сам автор помечает как «по знанию кода HA, не по прогону» — я не смог перепроверить его исполнением:python3 -c "import homeassistant"в песочнице отсутствует (ModuleNotFoundError), как и предупреждаетAGENTS.mdо песочнице. Формулировка честно ограничена, доказательство отложено на HA-harness в каноническом Linux CI (AC5 это явно называет) — не считаю это находкой, так как автор сам не выдаёт догадку за факт, а называет её пределы.
Находки
Нет. Ни одной находки High/Medium/Low после перепроверки кода — все утверждения ТЗ подтвердились чтением исходников, а не приняты на слово.
Одно наблюдение, не являющееся находкой (не блокирует, не требует
действия): AC1/AC2 называют тестовый приём «вызвать штатный
hass.async_stop()». Часть HA-интеграций в тестах вместо полной остановки
hass предпочитают hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) +
await hass.async_block_till_done() — легче для фикстуры, не завершает
цикл событий раньше, чем тест успеет прочитать файл store после. Это
стратегия теста, а не продуктовый контракт (§7.1: «стратегия тестов» —
то, что решает автор/ревьюер, не владелец), и ТЗ уже прямо резервирует
исполнителю свободу технических решений. Оставляю на усмотрение
реализации без возврата.
Что проверено и корректно
- Обязательные разделы §7.1 присутствуют по существу; сценарий и «что человек увидит» — продуктовые, одной фразой, без терминов реализации.
- Оба факта дефекта (голая
asyncio.create_taskу виртуальных ламп, отсутствие слушателяEVENT_HOMEASSISTANT_STOPво всём пакете) — перепроверены построчным чтением кода, а не приняты со слов автора. - Контракт из 5 пунктов однозначен, пункты не пересекаются и не противоречат друг другу; «идемпотентно» и «не создаёт новую запись при отсутствии pending-изменений» — прямо адресуют риск двойного cleanup, который сам ТЗ называет в разделе «Риски».
- AC1–AC5 каждый называет способ доказательства (HA-harness с конкретными шагами / unit-структурный свидетель / мутант) — критерий «указание способа доказательства» выполнен по всем пяти.
- AC5 заранее формулирует негативное доказательство (мутанты, реестр
которых уже существует в проекте,
scripts/mutation-registry.mjs) — это снижает риск пустой графы «чем краснеет» на код-ревью. single_config_entry: trueв манифесте снимает единственный архитектурный риск, который я держал в уме при чтении контракта (совместное состояниеTrailRecorderпри нескольких entries).- Технический паттерн (
hass.async_create_task/entry.async_create_background_task) уже используется в проекте для аналогичных фоновых задач — реализуемость контракта п.3 подтверждена прецедентом, а не гипотетична. - Открытых продуктовых вопросов нет — подтверждаю по тексту: единственное наблюдаемое поведение («последнее принятое состояние переживает штатный stop/restart») уже полностью и однозначно определено сценарием и контрактом, домысливать нечего.
- Release-артефакты, откат, отсутствие миграции — названы явно, без зазора для догадки.
Чего не проверял
- Гейты (
tsc --noEmit,npm test,npm run build,pytest tests_backend, инварианты, смоки,golden:verify) — не прогонял: этапspec, кода для #655 нет,git diff origin/dev...HEADпуст. Предмет код-ревью после реализации. - Не проверял исполнением HA-рантайм-утверждение про
EVENT_HOMEASSISTANT_STOPvsentry.async_shutdown— HA не устанавливается в песочнице (ModuleNotFoundError: homeassistant); сам автор ограничивает это утверждение как «по знанию кода, не по прогону» и переносит фактическую проверку на AC5/HA-harness в каноническом Linux CI — это его собственная явная граница, а не пропуск с моей стороны. - Не оспаривал конкретный числовой сценарий («увеличить окно debounce») как техническую деталь теста — оставляю технику на усмотрение реализации, как и позволяет §7.1 («стратегия тестов» — не продуктовый вопрос).
- Не проверял, потребует ли реализация протянуть
hass/entryвVirtualLightController.__init__изменения сигнатуры конструктора — архитектурная деталь модуля, прямо отданная исполнителю ТЗ.
Вердикт
Обязательные разделы полны и по существу, оба факта дефекта и архитектурные предпосылки контракта перепроверены чтением кода — расхождений не найдено. AC1–AC5 однозначны, каждый называет способ доказательства, негативное доказательство (AC5) спланировано заранее. Открытых продуктовых вопросов нет. Готово к разработке.
Вердикт: зелёный · заход r1 · блокирующих циклов 0/2 · High: 0 · Medium: 0 → в задаче
Материал раунда
- Ветка:
dev, коммит9a7020ce428e— ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала:
eec9247ab39136663b248226de905169bb3d18bcgit log --all --format='%H %T' | grep eec9247ab391 - Тело issue:
9592e83bda3f396fe43ee14eb6bc22b4860f8309d2c5e4e954dc1b90a293c348 - Вердикт конвейера:
green· High 0