mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
committed by
Sergey Matyunin
parent
563a850aac
commit
9baf533c90
@@ -0,0 +1,264 @@
|
||||
# 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 недоказуемым по существу; все зафиксированы с реконструкцией и оставлены на
|
||||
усмотрение автора при следующей правке файла, без нового цикла ревью.
|
||||
Reference in New Issue
Block a user