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

24 KiB
Raw Blame History

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 недоказуемым по существу; все зафиксированы с реконструкцией и оставлены на усмотрение автора при следующей правке файла, без нового цикла ревью.