Files
houseplan-card/docs/reviews/SPEC-REVIEW-419-r1.md
T
2026-09-02 16:08:44 +00:00

244 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.
# SPEC-REVIEW-419-r1
- Issue: https://github.com/Matysh/houseplan-card/issues/419
- Этап: ревью ТЗ (PROCESS.md §2.4)
- ТЗ: `docs/specs/419-area-snapshot-roster-guard.md`
- Материал: ветка `issue/419-area-snapshot-roster-guard`, SHA `153dc3f79401c55cb957f776566caaded85c9214`,
блоб ТЗ `eb5a7a352d742be601c7c695d27256f9af9bc0ad`
- Заход: r1 (первый; раздел «Унаследовано из r0» не применяется)
- Маршрут: standard (owner, комментарий 2026-09-02) — обоснованно: меняется
destructive lifecycle persisted metadata, риск выше лимита `small`
## Вердикт
**Зелёный.** High: 0, Medium: 0 (в скоупе и вне скоупа), Low: 2, обе сняты
ревьюером с записью (см. ниже) — не блокируют.
## Скоуп ревью
Полный разбор ТЗ (заход r1, дельты нет). Прочитано в порядке PROCESS.md:
`docs/SCOPE.md`, `AGENTS.md`, `PROCESS.md`, тело issue #419 и оба комментария
владельца (аналитика с Q1–Q3 и defaults, публикация ТЗ), само ТЗ
`docs/specs/419-area-snapshot-roster-guard.md`, канонический раздел
«Marker Area provenance (#126)» в `docs/CONFIG-COMPATIBILITY.md`.
## Как проверялось
Ревью ТЗ на этапе spec не читает код на предмет корректности реализации — код
ещё не написан. Но каждое фактическое утверждение проблемы и контракта сверено
с текущим состоянием репозитория, чтобы отличить обоснованный анализ от
догадки, выданной за факт:
- `src/device-area-relocation.ts` (полностью, 266 строк) — подтверждён ровно тот
дефект, который описывает issue: `resolveDeviceAreaRelocations()` (строки
144–160) сравнивает `snapshot` только с `liveIds`/`liveBindings`, построенными
из `options.devices` — presentation-проекции, а не с реестром;
- `src/ha-binding-status.ts` (полностью) — подтверждено, что `authoritative`
(строка 141) гарантирует только успешный парсинг обоих WS-ответов
(`config/device_registry/list`, `config/entity_registry/list`) в массивы, но
не защищает от пустого/усечённого, но технически валидного кадра; подтверждено
существование `snapshot.devices`/`snapshot.entities` (полный реестр, не
отфильтрованный), `snapshot.revision` (инкрементируется на каждой успешной и
неуспешной перезагрузке) и готового механизма `refreshHaRegistries()` для
«одного контрольного refresh» из AC9 — эти источники данных существуют и
доступны, контракт §1–§3 ТЗ на них реализуем;
- `src/devices.ts:1118` (`buildDevices`) — подтверждена конкретная причина, по
которой живой device легитимно отсутствует в `devices[]`: `if (!area ||
!areaToSpace[area]) continue;` — устройство без Area или с Area, не
сопоставленной ни одному space, отбрасывается на входе, до любых
показанных/скрытых фильтров;
- `src/space-card.ts:445-465` (`houseplan-space-card`, вторая поверхность из
«Сценария») — подтверждено, что Static-поверхность читает тот же
`resolveDeviceAreaRelocations()`, но использует только `.relocateIds` для
рендера и никогда не пишет `marker_area_snapshot` обратно. Дефект добирается
до неё не через собственный вызов резолвера, а через общий персистентный
снапшот, который портит write-путь `houseplan-card.ts`. Поэтому список
«Затрагиваемые файлы» ТЗ корректно не включает `space-card.ts` — это
потребитель уже испорченных данных, а не второй источник дефекта;
- `test/device-area-relocation.test.mjs` (полностью, 240 строк) и
`demo/smoke_area_relocation.mjs` (структура, включая перехват
`config/device_registry/list`/`config/entity_registry/list` на строках
353–354) — подтверждена техническая возможность добавить параметризованную
матрицу и точный счётчик WS-вызовов реестра, как того требуют AC1–AC9 и план
автотестов;
- `docs/CONFIG-COMPATIBILITY.md`, раздел «Marker Area provenance (#126)» —
сверены заявления ТЗ про формат/лимит/поведение import-путей: ТЗ говорит
«Full import/export и space-only import сохраняют контракты #126 без
изменений» — это означает «поведение этих путей не меняется этой задачей», а
не «данные переживают cross-source import» (тот кейс и так по канону
выбрасывает карту) — формулировка не вводит в заблуждение при внимательном
чтении, но могла быть однозначнее (см. Low L1 ниже, не блокирует);
- git-история `#126`/`#403`/`#406` (`git log --grep`) подтверждает, что все три
контракта существуют как отдельные, ранее принятые задачи над тем же модулем,
что соответствует ссылкам ТЗ.
## Проверка обязательных разделов (PROCESS.md §7.1)
Все обязательные разделы присутствуют и в правильном порядке: сценарий · что
человек увидит до/после · проблема · скоуп/не-скоуп · контракт поведения · UX ·
модель данных и миграция · i18n · критерии приёмки AC1–AC14 с доказательством ·
план автотестов · риски · откат · release-артефакты. Плюс обязательный блок
«принято предположительно, поменять свободно» — присутствует и по существу
касается только нерешаемых пользователем технических деталей (сигнатура
резолвера, хранение runtime-кандидатов, раздельный empty-guard, переиспользование
существующего debounce), что соответствует PROCESS.md §7.1.
**Продуктовые первые два раздела.** Персона названа (Home admin), поверхности —
обе («полная карточка и houseplan-space-card»), момент — во время
запуска/перезапуска/обновления HA. «Что человек увидит» — одной фразой без
терминов реализации, до и после. Соответствует шаблону.
## Проверка однозначности и доказуемости AC
14 критериев приёмки, каждый — проверяемое утверждение с названным способом
доказательства (`unit`, `browser smoke`, `parameterized unit`,
`mutation-gate.mjs`, «локальные гейты процесса», `check-docs`/CI/diff manifest).
Ни один не сформулирован как «работает корректно» без критерия. Технически
реализуемы все — см. «Как проверялось» выше:
- AC1–AC4 (положительное доказательство жизни: полный Device/Entity Registry,
exact live state включая `unavailable`/`unknown`, живой сохранённый marker) —
каждый источник подтверждён существующим в коде;
- AC5–AC7 (два подтверждения, восстановление между проходами, повтор одной и
той же revision не считается вторым подтверждением) — однозначны: любая
отличающаяся успешная непустая authoritative revision закрывает второе
подтверждение, независимо от того, вызвана она контрольным refresh (AC9) или
сторонним событием реестра — оба AC не противоречат друг другу при точном
прочтении (см. Low L2);
- AC8 (limited/error и reload/remount не подтверждают и не наследуют) —
согласуется с §4 контракта и Q3-default владельца;
- AC9 (ровно один confirmation refresh, без reload-loop) — технически
реализуемо через существующий `refreshHaRegistries()`/`scheduleReload()` с
80 мс debounce;
- AC10 (существующие пути #126/#403/#406 не задерживаются) — граница явно
описана в §5 контракта: явное удаление, rebind, explicit/ineligible,
normal relocation остаются однопроходными;
- AC11 (отказ записи не портит идемпотентность) — согласуется с уже
реализованным в коде паттерном restore-on-failure (`houseplan-card.ts:5267-
5331`), контракт ТЗ его не переопределяет, только защищает точку принятия
решения раньше по конвейеру;
- AC12 (три целевых мутанта) — прямо соответствуют трём защищаемым инвариантам
контракта (§1 «presentation ≠ registry», §3 «одного отсутствия мало», §2
«пустой namespace не источник»);
- AC13–AC14 — стандартные гейты процесса и синхронизация документации/changelog,
ничего специфичного к этой задаче не требуется сверх обычного.
## Проверка «догадка вместо решения»
Не найдено ни одного заявления о поведении, которое не подтверждается ни
существующим документом, ни исходным кодом, ни явно не помечено как
предположение технического уровня. Ключевые фактические утверждения проблемы
(«authoritative гарантирует только форму ответа, не его полноту»; «devices —
presentation-проекция, а не реестр»; «auto-device без сопоставленной Area
законно отсутствует в devices») — все верифицированы против кода в разделе «Как
проверялось» выше. Продуктовые defaults Q1–Q3 взяты дословно из ответа
владельца от 2026-09-02, а не изобретены автором.
## Продуктовое рассуждение (SCOPE.md)
Задача закрывает job **J6** «Keep the plan true as the home evolves» —
конкретно ветку «переезд area» из #126: без этого фикса временный сбой реестра
при рестарте HA способен молча стереть провенанс переезда, из-за чего плановая
карточка перестаёт отличать «устройство переехало» от «устройство обнаружено
впервые». UX-раздел ТЗ прямо и правильно формулирует, что исправление
намеренно тихое — новых элементов интерфейса, уведомлений, настроек нет, что
соответствует духу «never delete a user's file on an inference»
(`docs/SCOPE.md`): тот же принцип «wasted state дешевле, чем потерянные
данные», применённый здесь не к файлу, а к lifecycle-метаданным. Изменение не
ухудшает ни одну соседнюю персону/сценарий: Static-карточка (household members,
kiosk) получает тот же эффект косвенно и без нового кода в своей ветке (см.
выше), View/kiosk/touch не затронуты явно и по разделу «Touch, accessibility и
security» ТЗ.
## Находки
Находок, блокирующих цикл (High) или требующих правки в скоупе без блокировки
(Medium), нет.
### Low (сняты ревьюером с записью, не блокируют)
- **L1.** Формулировка «Full import/export и space-only import сохраняют
контракты #126 без изменений» неоднозначна при беглом чтении: можно понять
как «данные переживают import», хотя по канону (`CONFIG-COMPATIBILITY.md`,
«Marker Area provenance») cross-source full import и space-only import и
раньше не переносили эту карту. При внимательном чтении фраза корректна
(«контракт», то есть поведение этих путей, не меняется этой задачей) и не
противоречит AC. Снимается без правки: реализация не может ошибиться иначе,
так как поведение уже зафиксировано существующим кодом/документом, который
эта задача не трогает (не-скоуп прямо это исключает).
- **L2.** Контракт «второе подтверждение» (§3 п.4, AC6) и «ровно один
confirmation refresh» (AC9) не объявляют явно одной строкой, что confirmation
может прийти НЕ от специально запрошенного refresh, а от любой следующей
отличающейся authoritative revision, случившейся по другой причине (внешнее
registry-событие). Оба текста по отдельности читаются однозначно и не
противоречат друг другу при точном прочтении (AC9 ограничивает число
*запрашиваемых* карточкой обновлений, а не число революций, которые вправе
засчитаться), но явная фраза избавила бы разработчика от лишнего
сомнения. Снимается без правки ТЗ: тесты AC5–AC7/AC9 достаточно точны, чтобы
реализация и код-ревью зафиксировали единственное согласованное поведение;
безопасность контракта (никогда не удалять по одному наблюдению) не зависит
от источника второй revision в любом случае.
## Что проверено и корректно
- Маршрут `standard` обоснован верно — задача меняет destructive lifecycle
persisted metadata, критерии лёгкого трека (`docs/PROCESS.md` §5) не
выполняются одновременно (минимум «нет нового UX-контракта» под вопросом
из-за меняющегося поведения записи, и уж точно объём контракта поведения не
укладывается в «сложность ≤3»);
- Q1–Q3 из аналитики перенесены в контракт ТЗ дословно (§2, §3, §4 соответствуют
принятым defaults), никаких расхождений с решением владельца от 2026-09-02;
- Скоуп и не-скоуп взаимно непротиворечивы и корректно исключают: смену формата
`marker_area_snapshot`, лимит 20 000, uborку `known_devices`/`new_device_ids`,
общий аудит HA Registry API — ничего из этого не требуется контрактом §1–§5;
- Все AC привязаны к реально существующим точкам данных (`hass.states`, полный
Device/Entity Registry через `haRegistrySnapshot`, сохранённые markers), а не
к гипотетическим API;
- План автотестов покрывает все 14 AC без дыр: каждая матрица unit-кейсов (1–9)
и каждый browser-smoke сценарий адресует конкретный AC;
- Откат — простой revert коммита, без флага; персистентная модель не меняется,
что подтверждено сверкой с текущим TS-типом `MarkerAreaSnapshot` в
`src/device-area-relocation.ts:1-9`;
- Release-артефакты названы полностью: оба changelog, нормативная документация,
обязательный Docs screenshots run (канонический источник фингерпринта), явно
«golden не меняется».
## Чего не проверял (и почему не требовалось на этапе spec)
- Код ещё не написан — код-ревью (PROCESS.md §2.7) на следующем этапе отвечает
на вопрос «работает ли реализация»; здесь оценивалась только выполнимость и
однозначность контракта;
- Не запускал `npx tsc --noEmit`/`npm test`/`npm run build` — на этапе ТЗ
продуктовый код не менялся (класс C, документация), гейты кода к этому этапу
не относятся;
- Не проверял `demo/smoke_area_relocation.mjs` исполнением — только структуру
файла для оценки технической реализуемости AC9 (счётчик WS-вызовов) и
существующего перехвата `config/device_registry/list`/`entity_registry/list`;
- Не сверял точный будущий сигнатурный контракт `resolveDeviceAreaRelocations()`
и совместимость с вызовом в `space-card.ts` — прочитано, что несовместимости
по данным нет (Static не пишет снапшот и не зависит от orphan-sweep решений),
но точная форма новых параметров прямо оставлена на усмотрение реализации
(«принято предположительно, поменять свободно») и будет предметом код-ревью;
- Гейты `docs`/`check-docs`, golden, performance, backend — не относятся к
этапу ТЗ (документация/код ещё не менялись сверх самого файла ТЗ).
## Унаследовано из r<N-1>
Не применяется: это первый заход (r1), предыдущего раунда нет.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `issue/419-area-snapshot-roster-guard`, коммит `153dc3f79401` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет.
- Дерево материала: `1ad31edcb0a80464e1e7b1a16839b69449c910ff`
```
git log --all --format='%H %T' | grep 1ad31edcb0a8
```
- ТЗ `docs/specs/419-area-snapshot-roster-guard.md`, блоб `eb5a7a352d742be601c7c695d27256f9af9bc0ad`
```
git log --all --find-object=eb5a7a352d742be601c7c695d27256f9af9bc0ad -- docs/specs/419-area-snapshot-roster-guard.md
```