Files
houseplan-card/docs/reviews/SPEC-REVIEW-117-r1.md
T
2026-08-19 02:11:36 +03:00

265 lines
24 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-117-r1
- **Issue:** https://github.com/Matysh/houseplan-card/issues/117
- **ТЗ под ревью:** `docs/specs/117-registryless-opening-entity.md` (коммит `cc2298816b8ca4e41a41830fef69fd2ee1cb7ed9`)
- **Роль:** ревьюер ТЗ (не автор), этап `S4-spec-review`
- **Трек:** обычный (не `small`) — аналитика оценила сложность 4/10, риск 5/10
(> 3), поверхности: opening entity picker/resolver, View state/action,
unit/smoke/docs — больше одной поверхности; лёгкий трек корректно не применён
- **Цикл:** r1/4
## Скоуп ревью
Проверялось соответствие ТЗ:
- `docs/SCOPE.md` — попадание в Core user jobs, отсутствие расширения скоупа;
- `PROCESS.md` §2.4, §2.5 (DoR), §7.1 (обязательные разделы), §3.18 и §12
(запреты, включая «Medium как TODO в тексте ревью»);
- `AGENTS.md` — классы файлов, ветка, трейлеры;
- `docs/USER-GUIDE.ru.md` — терминология «Контактный датчик» / «Замок» /
«проём»;
- issue #104 — откуда #117 выделен решением владельца, и не пересекается ли
ТЗ с уже закрытым скоупом #104 (marker tombstone);
- фактическому состоянию кода (`src/ha-binding-status.ts`, `src/houseplan-card.ts`)
— на предмет того, что технические утверждения ТЗ не являются непроверенной
догадкой, а не описанием несуществующего поведения.
## Как проверялось
1. Прочитано тело issue #117 (автор — владелец, `Matysh`, 2026-08-13) и оба
комментария: аналитика (2026-08-14, P2/bug/обычный трек, продуктовых
вопросов нет) и заведение ТЗ (2026-08-15, ссылка на файл и коммит).
2. Прочитан весь текст `docs/specs/117-registryless-opening-entity.md`
(223 строки) построчно.
3. Прочитан `src/ha-binding-status.ts:308-545`: подтверждено, что
`activeRegistryHass()` уже сознательно сохраняет live state registry-less
сущности (комментарий в коде на строках 342-346 прямо это объясняет), а
`renderOpeningEntityAvailable()` (строка 538) требует одновременно
`.entities[entityId]` **и** `.states[entityId]` — то есть расхождение,
которое ТЗ называет причиной бага, реально существует в коде, а не
придумано. Более того, комментарий к функции на строке 536 уже сегодня
говорит: «Registry-less render parity is #117» — само ТЗ не изобретает
этот план, а закрывает уже отмеченный в коде долг.
4. Прочитан `src/houseplan-card.ts:16256-16380`: подтверждено, что и
`_openingAmt()`, и `_renderOpeningLocks()` идут через единственную точку
`_renderOpeningEntityAvailable()` → `renderOpeningEntityAvailable(this._renderPlanHass, eid)`
— то есть один фикс на уровне `ha-binding-status.ts` действительно
закрывает contact и lock одновременно, как заявляет ТЗ, без риска
рассинхронизации двух копий логики.
5. Прочитано `_captureRenderDeviceSnapshot()` (`src/houseplan-card.ts:3559-3673`):
подтверждено, что `opening.contact`/`opening.lock` уже входят в `entityIds`
при захвате snapshot (строки 3595-3596) и что `planHass` в этом снимке —
результат `activeRegistryHass()` (строка 3553/3574) — то есть registry-less
state уже присутствует в замороженном кадре сегодня; баг только в лишней
проверке `.entities[entityId]` при чтении, как и утверждает §2 ТЗ.
6. Прочитан `src/houseplan-card.ts:16269-16284` (`_renderEntityAvailable` vs
`_renderOpeningEntityAvailable`): подтверждено, что общий
`_renderEntityAvailable()` (используется для прочих marker-биндингов) не
входит в фикс — ТЗ §4 прямо исключает «generic поддержку registry-less
entities во всех marker bindings», и это согласуется с кодом: два разных
потребителя, значит сужение скоупа не оставляет незамеченного разрыва.
7. Прочитан `docs/USER-GUIDE.ru.md:409-444` («Настройки проёма», «Замок») —
терминология ТЗ («контакт», «замок», «info card», «unlock confirmation»)
совпадает с пользовательским словарём; текущая формулировка «Контактный
датчик и замок — самостоятельные точные привязки проёма» уже описывает
независимость от marker (#104), но ничего не говорит о registry —
подтверждает, что баг сегодня не задокументирован пользователю и правка
§14 (уточнить в user-guide поддержку YAML-сущности без `unique_id`)
осмысленна.
8. Сверено, что упомянутые в плане тестирования файлы существуют:
`test/ha-binding-status.test.mjs`, `test/render-device-snapshot.test.mjs`,
`demo/smoke_opening_binding.mjs`, `demo/smoke_lock_action.mjs`,
`demo/smoke_lock_invariant.mjs` — план тестирования не ссылается на
несуществующее покрытие.
9. Прочитано тело issue #104: подтверждено, что #104 занимался независимостью
opening-биндинга от marker tombstone (не от registry-строки) — #117 не
пересекается с уже закрытым скоупом #104, а закрывает отдельно вынесенную
часть, как и написано в issue.
10. Проверена запись `docs/specs/README.md:85` — добавлена тем же коммитом,
ссылка issue ↔ ТЗ двусторонняя.
11. Автор issue — владелец (`Matysh`), легитимность входа в процесс вопросов
не вызывает.
## Обязательные разделы (§7.1 PROCESS.md)
| Раздел | Есть | Комментарий |
|---|---|---|
| Сценарий (персона/поверхность/момент) | ❌ | нет отдельного раздела; персона и поверхность не названы явно нигде в документе |
| Что человек увидит до/после (без терминов реализации) | ❌ | ближе всего — последняя фраза §1 («сохранённый contact/lock не влияет на отрисованный проём»), но она внутри технического абзаца с `hass.states`/render path, а не отдельной продуктовой фразой |
| Проблема | ✅ | §1–2, с точной причиной по коду |
| Скоуп / не-скоуп | ✅ | §3 (Цели) / §4 (Не входит) |
| Контракт поведения | ✅ | §5, §8, §9 |
| UX | ~ | нет отдельного раздела, но §6 (матрица), §8, §9 покрывают поведение по существу |
| Модель данных и миграция | ✅ | §10 («config/storage/API не меняются») |
| i18n | ✅ | §14 («новых RU/EN строк нет») |
| AC1…ACn с доказательством | ~ | §11, 10 пунктов, но без инлайн-тега типа доказательства у каждого (см. Low-2) |
| План автотестов | ✅ | §12 |
| Риски | ✅ | §15 |
| Откат | ✅ | §15 |
| Release-артефакты | ✅ | §14 |
Два обязательных продуктовых раздела (сценарий, что человек увидит) в виде
отдельных секций отсутствуют — см. Low-1. Технической неоднозначности это не
создаёт: контент восстановим из §1/§9 и подтверждён чтением кода (см. «Как
проверялось»), поэтому не поднимаю до High/Medium (см. обоснование в Low-1).
## Находки
Находок уровня **High** и **Medium** нет.
### Low-1 — нет отдельных разделов «Сценарий» и «Что человек увидит»
**Файл:** `docs/specs/117-registryless-opening-entity.md:9-17` (§1)
PROCESS.md §7.1 требует эти два раздела первыми и объясняет почему: «ТЗ,
которое не может ответить на эти два вопроса, описывает работу, а не
изменение продукта». В этом ТЗ persona/surface/moment нигде не названы явно,
а «что увидит человек» растворено в техническом абзаце про `hass.states` и
render path.
Восстановленный ответ (проверяемый по SCOPE.md и коду, не по догадке):
персона — **Home admin** привязывает live YAML-сущность без `unique_id`
(например, `binary_sensor` без `unique_id`) к контакту/замку проёма в Plan
editor, picker её принимает; после сохранения все три персоны, смотрящие на
**View** (включая kiosk-планшет), видят проём/замок, который не реагирует на
реальное изменение состояния сущности, хотя в HA оно меняется. После фикса —
тот же проём ведёт себя как с обычной зарегистрированной сущностью.
**Решение ревьюера:** Low, не блокирует. Ни один AC не становится
двусмысленным из-за отсутствия этих разделов, и содержание однозначно
восстанавливается из §1/§9/кода — не тот случай, когда «домыслил ревьюер»
подменяет решение владельца, потому что сама сцена уже зафиксирована в issue
и SCOPE.md (View — продукт для двух персон), а не изобретена сейчас. Оставляю
на усмотрение автора: можно добавить двумя короткими фразами при следующей
правке файла, отдельного цикла ревью это не требует.
### Low-2 — AC1–AC10 (§11) не несут инлайн-тег способа доказательства
**Файл:** `docs/specs/117-registryless-opening-entity.md:145-158`
DoR (`PROCESS.md` §2.5) требует: «AC1…ACn — ... у каждого указано, чем он
доказывается: `unit` / `backend` / `smoke` / `golden` / «ревью кода»». Ни один
из десяти пунктов §11 не несёт такой пометки; способ доказательства нужно
реконструировать из §12 «План тестирования» отдельно.
Реконструкция (сверена с реальными файлами, см. «Как проверялось», п.8):
| AC | Доказательство |
|---|---|
| AC1 (registry-less contact меняет presentation) | `smoke`: новый YAML-like `binary_sensor` сценарий (§12) |
| AC2 (registry-less lock badge) | `smoke`: новый YAML-like `lock` сценарий (§12) |
| AC3 (frame использует projection, не raw hass) | `unit`: mutation-guard «вернуть `.entities[entityId]` — YAML case красный» (§12) — единственный AC, где мутационный тест сам по себе и есть доказательство, а не просто регрессия |
| AC4 (registry entity — прежнее поведение) | `smoke`/`golden`: существующие opening-сценарии без изменений |
| AC5 (disabled/orphan/missing остаются unavailable) | `unit`: расширенная матрица в `ha-binding-status.test.mjs` |
| AC6 (limited-registry live entity работает) | `unit`: тот же файл, ветка `authoritative: false` |
| AC7 (marker tombstone не блокирует) | `smoke`: «same entity tombstoned as marker» (§12) |
| AC8 (lock security/confirmation не меняются) | не назван явно; фактически покрыт существующими `demo/smoke_lock_action.mjs` и `demo/smoke_lock_invariant.mjs` (регрессия, п.3 «Регрессия» §12) плюс «ревью кода» |
| AC9 (state update без geometry/config rebuild) | `smoke`: «render snapshot atomicity on state tick» (§12) |
| AC10 (существующие opening golden/interactions без регресса) | `golden`/`smoke`: регрессионный прогон существующих сценариев (§12, явно уточнено, что golden-эталоны не переакцептуются) |
Каждый AC в итоге прослеживается к конкретному, реально существующему тесту
или к явно допустимому DoR методу «ревью кода» (AC8). Ни один пункт не
остаётся недоказуемым по существу — разрыв чисто в оформлении: тег не
проставлен инлайн, как это было сделано, например, в предыдущем ТЗ #123 (13/13
AC с явным типом).
**Решение ревьюера:** Low, не блокирует. Реконструкция выше снимает риск
«недоказанного AC» для этого цикла; рекомендую при следующей правке файла
добавить тег каждому AC (особенно явно объявить AC8 как «ревью кода +
`smoke_lock_action.mjs`/`smoke_lock_invariant.mjs`», а не оставлять
подразумеваемым). Отдельного issue не завожу: это дефект оформления текущего
ТЗ, а не самостоятельная будущая работа — заводить для него `S1-new` issue
было бы имитацией процесса, а не пользой.
### Low-3 — матрица (§6) не содержит отдельной строки для registry-less entity в состоянии `unavailable/unknown`
**Файл:** `docs/specs/117-registryless-opening-entity.md:79-95`
Матрица явно разбирает «registry entity `unavailable/unknown`» (строка 85), но
не добавляет параллельную строку для «YAML/no registry row entity в
`unavailable/unknown`». Пояснение под таблицей («unavailable/unknown здесь
означает, что exact reference существует... #117 не подменяет эти states
active/open») по формулировке относится к обеим ветвям, и это подтверждено
чтением кода: `openingAmount()` (`src/logic.ts:316`) не различает
registry-статус сущности, работает только со строкой state — поэтому поведение
для YAML-сущности в `unavailable` идентично зарегистрированной. Риска для
корректности нет, только асимметрия изложения.
**Решение ревьюера:** Low, снимаю с этой записью, правка не обязательна.
## Что проверено и корректно
- Соответствие `docs/SCOPE.md`: правка внутри уже закрытых J4 (онбординг/editor)
и J6 («Keep the plan true as the home evolves», два редактора, drag/resize,
editable icon rules) — это регрессионный/пограничный баг внутри принятой
функциональности opening-биндинга, а не новая фича и не расширение скоупа.
- Технический диагноз (§2) подтверждён построчным чтением `ha-binding-status.ts`
и `houseplan-card.ts` — не голословное утверждение автора; комментарий в коде
уже сегодня ссылается на #117 как на известный долг.
- Единая точка фикса (`renderOpeningEntityAvailable`) реально закрывает и
contact, и lock одновременно — проверено по вызывающему коду, а не
предположено.
- Матрица поведения (§6) корректно защищает все смежные случаи, которые могли
бы регрессировать: явный `disabled_by` у сущности и у устройства,
authoritative orphan, отсутствующий state, limited-registry режим — все эти
ветки уже реализованы в `activeRegistryHass()`/`resolveHaBindingStatus()` и
проверены построчно (см. «Как проверялось», п.3).
- Независимость от marker tombstone (AC7) корректно унаследована из #104:
`renderOpeningEntityAvailable` не обращается к `isRemovedPlanEntity()`, в
отличие от соседнего `_renderEntityAvailable()` — сужение скоупа (§4,
«generic поддержка... во всех marker bindings» — не входит) не оставляет
незамеченного разрыва, оба потребителя разделены в коде.
- Lock security и confirmation (§9) сформулированы дословно в терминах
существующего инварианта `docs/SCOPE.md` («The lock invariant, stated
precisely») — не ослабление и не новая семантика actuation.
- Раздел 16 «Принятые технические предположения» корректно отделяет свободно
изменяемые технические решения от продуктового контракта; ни одна догадка не
выдана за факт без пометки — я не нашёл ни одного утверждения о поведении,
которое не следует из кода, SCOPE.md или явно принятых допущений.
- Открытых продуктовых вопросов действительно нет: единственные развилки
(registry vs registry-less, authoritative vs limited access, disabled vs
orphaned) — все технические, не продуктовые, и решены автором согласно
правилу §7.1 «всё, чего пользователь не наблюдает, агенты решают сами».
- Терминология (USER-GUIDE.ru.md) не изобретена заново — «контакт», «замок»,
«info card», «unlock confirmation» совпадают с действующим словарём.
- Совместимость (§10): config/storage/API не меняются, существующие
registry-backed openings объявлены pixel-identical, touch/kiosk жесты не
меняются, i18n ключей не добавляется — всё согласуется с
`docs/CONFIG-COMPATIBILITY.md` и `docs/TOUCH-SUPPORT.md` (нет заявленного
влияния, что и требуется).
- Реестр `docs/specs/README.md` обновлён тем же коммитом, ссылка issue ↔ ТЗ
двусторонняя; issue заведён владельцем — легитимность входа в процесс не
вызывает вопросов.
- Golden явно не требует переакцептации (существующие baseline не меняются) —
корректное сужение release-артефактов, не попытка обойти гейт.
## Чего не проверял
- Не запускал никаких автотестов или смоков — на этапе `spec` это не
требуется; существование файлов, упомянутых в плане тестирования, проверено
чтением (`test/ha-binding-status.test.mjs`, `test/render-device-snapshot.test.mjs`,
`demo/smoke_opening_binding.mjs`, `demo/smoke_lock_action.mjs`,
`demo/smoke_lock_invariant.mjs`), не исполнением.
- Не проверял реальное поведение реальной HA-инсталляции с YAML-сущностью без
`unique_id` — это будет предметом browser smoke на код-ревью (§12 плана
тестирования), не ревью ТЗ.
- Не проверял `src/logic.ts:openingAmount()` построчно на предмет всех
инвариантов инверсии/leaf-угла — доверяю формулировке §8 «state проходит
текущий `openingAmount(...)`; invert сохраняется» как описанию уже
существующего, не нового контракта; это не предмет этого фикса (§4: «не
входит в задачу» новые opening types или geometry).
- Не проверял historical переписку по ревью ТЗ #104 (сам документ ревью #104
не найден отдельно) — доверяю формулировке issue #117 «решением владельца
registry-less render исключён из scope #104», подтверждённой владельцем в
комментарии аналитики (нет открытых вопросов/несогласия по этой границе).
- Не проверял производительность — ТЗ явно заявляет «нет» (§14), и правка не
трогает горячий путь по кадрам иначе, чем чтением одного дополнительного
boolean на существующей projection.
## Вердикт
Зелёный. High: 0, Medium: 0. Три находки Low (отсутствие двух обязательных
продуктовых разделов; отсутствие инлайн-тегов доказательства у AC1–AC10;
асимметрия одной строки матрицы) — ни одна не блокирует и не оставляет
AC недоказуемым по существу; все зафиксированы с реконструкцией и оставлены на
усмотрение автора при следующей правке файла, без нового цикла ревью.