# SPEC-REVIEW-29-r1 - Issue: https://github.com/Matysh/houseplan-card/issues/29 - Этап: spec (PROCESS.md §2.4) - Заход: r1 · блокирующих циклов израсходовано 0 из 4 - ТЗ: `docs/specs/029-device-inbox-lifecycle.md` - SHA материала ревью: `a6ce1ae7e534fa4b41fcc0ab93e5ed95a693c4c1` (ветка `issue/29-device-inbox-lifecycle`) - Трек: обычный (не `small`), лимит циклов ревью ТЗ — 4. Это первый заход ревью по этой задаче — предыдущего вердикта в issue нет, раздел «Унаследовано» не применяется, разбор полный. ## Скоуп разбора Проверено: 1. `docs/SCOPE.md` — соответствие job'ам J4/J6, отсутствие выхода за продуктовую рамку (out-of-scope список, standing rules). 2. `AGENTS.md` и `PROCESS.md` §2.4, §7.1 — формальные требования к ТЗ и формат вердикта. 3. Тело issue #29 и все комментарии, включая повторную актуализацию анализа от 2026-08-28 и решение владельца по Q1. 4. `docs/USER-GUIDE.ru.md` (терминология «Скрытые и деактивированные», «Редактор устройств», раздел про скрытие/удаление) и `docs/FILTERING.md` (канонический контракт hidden/removed/tombstone/entity-ownership). 5. Исходный код: `src/houseplan-card.ts` (`_bindingCandidates`, `new_device_ids`, `_maybeRebuildDevices`, `.slice(0, 200)` дважды), `src/ha-binding-status.ts` (`HaBindingStatus.kind`), `src/types.ts` (`marker.hidden`, `marker.removed`), `src/i18n/ru.json` и `en.json`. 6. Существующие тесты, упомянутые в AC11/§16: `test/devices.test.mjs`, `test/ha-binding-status.test.mjs`, `test/device-presentation*.test.mjs`, `demo/smoke_hidden_flag.mjs`, `demo/smoke_binding_picker.mjs` — существуют, содержимое `smoke_hidden_flag.mjs` прочитано целиком. 7. `docs/CONFIG-COMPATIBILITY.md` — беглая проверка на отсутствие противоречий заявлению «нет новых persisted-полей». Ревью — на этапе ТЗ: продуктового кода нет, автотесты не запускались (их ещё не существует), гейты §8 к этому этапу не относятся. ## Метод проверки Каждое фактическое утверждение ТЗ о текущем поведении продукта («До», §3, §7, §8, §9, §10.1) сверено с кодом или каноническим документом, а не принято на слово автора. Отдельно проверено, есть ли в тексте утверждение о поведении, которого нет ни в одном документе и которое не помечено как предположение (раздел 21 ТЗ). ## Находки ### M1 (Medium, в скоупе) — исчезновение призрачного показа скрытых/HA-disabled маркеров на плане не названо как продуктовое решение **Файл:** `docs/specs/029-device-inbox-lifecycle.md`, §10.1 (строки 216–222). Сегодня редактор устройств может показать скрытые и HA-disabled маркеры прямо на плане, в их реальной позиции, призраками — режим переключается кнопкой «Скрытые и деактивированные» (`docs/FILTERING.md` строки 84–91, `docs/USER-GUIDE.ru.md:178` — «доступны скрытые маркеры», перетаскиваются; подтверждено в `demo/smoke_hidden_flag.mjs`, где ghost рендерится в `_setMode('devices')` с `_showHidden = true` и клик по нему открывает диалог). Это единственный способ увидеть, ГДЕ на плане сидит скрытое или деактивированное устройство, не отменяя его скрытость. ТЗ прямо убирает этот режим: «Скрытые markers больше не рисуются поверх плана постоянным локальным режимом: доступ к ним даёт каталог» (§10.1). Но каталог (§10.2–10.4) не даёт эквивалента: `Find` явно доступен «только если marker реально отрисовывается» (§10.4), а для строк категории «Скрытые» и для «На плане, временно не отображается из-за HA status» primary/secondary действия — это «Показать»/«Настроить», не позиция на плане. Значит, чтобы увидеть, где стоит скрытый маркер, администратору придётся сначала его показать (что меняет конфиг), посмотреть, и при необходимости скрыть обратно — вместо непосредственного просмотра. Это видимое пользователю изменение объёма функциональности («какая персона что видит и делает» — ровно тот класс вопросов, который согласно PROCESS.md §7.1 задаётся владельцу или явно фиксируется как принятое предположение в §21). В тексте ТЗ оно подано как самоочевидный технический побочный эффект объединения кнопок, а не как решение, которое можно оспорить: обоснование «устраняет состояние панели, неочевидное после возврата в редактор» — это плюс нового дизайна, но оно не адресует потерю прямого просмотра позиции. **Не High**, потому что: обходной путь существует (Показать → посмотреть/ перетащить → Скрыть), это не потеря данных и не поломка AC — просто непроверенное продуктовое допущение, которое дёшево закрыть на этом этапе. **Как закрыть в этом же цикле (на выбор автора):** либо явно вынести это в блок §21 как предположение, которое ревьюер/владелец может оспорить, с описанием обходного пути; либо задать это владельцу одним пакетным вопросом с предложенным дефолтом (например: «Find для скрытой/disabled строки временно подсвечивает позицию на плане не снимая hidden» как альтернативный дизайн); либо сознательно сохранить упрощённый вариант, но явно назвать компромисс и обходной путь в §2 «До/После» и в §18 «Риски». ### M2 (Medium, в скоупе) — устаревающая строка i18n не включена в план обновления **Файлы:** `src/i18n/ru.json:673`, `src/i18n/en.json:673` (`marker.hide_tip`). Текущий текст подсказки при скрытии маркера дословно ссылается на кнопку, которую это ТЗ удаляет: RU — «Вернуть его можно через кнопку «Скрытые и деактивированные» в редакторе устройств»; EN — `Restore it through "Hidden and disabled" in the device editor`. §10.1 заменяет обе кнопки («Добавить» и «Скрытые и деактивированные») одной кнопкой «Устройства» (`devbar.add`, `devbar.show_all` перестают существовать в UI в текущем виде). Раздел 14 (i18n) перечисляет только **новые** ключи и не содержит пункта «обновить существующие ключи, ссылающиеся на удаляемые элементы интерфейса». Если реализовать ТЗ как написано, `marker.hide_tip` останется нетронутым и после релиза будет указывать пользователю нажать несуществующую кнопку — конкретный, проверяемый дефект, а не гипотетический. **Как закрыть:** добавить в §14 явный пункт «`marker.hide_tip` (en+ru) обновляется, чтобы указывать на новую точку входа «Устройства»» (и проверить, нет ли других строк с той же ссылкой — быстрый `grep` по `show_all`/«Скрытые и деактивированные» в `src/i18n/*.json` показывает, что это единственная пара строк такого рода, кроме самих `devbar.*` /`title.show_all`, которые и так меняются по §10.1). Обе находки Medium, в скоупе задачи (правки в самом файле ТЗ) — по PROCESS.md §2.4/§2.7 они не создают отдельный issue (решение владельца 2026-08-19, #202) и возвращают ТЗ автору с жёлтым вердиктом. ## Что проверено и признано корректным - **Формальные разделы §7.1** — сценарий, «что человек увидит до/после», проблема, скоуп/не-скоуп, контракт поведения, UX, модель данных и миграция, i18n, AC1…AC11 с указанием способа доказательства, план автотестов, риски, откат, release-артефакты — все присутствуют. - **AC1–AC11** однозначны и у каждого назван способ доказательства (unit/smoke/golden/review); ни один не описывает недоказуемое поведение. - **Q1 владельца** (классификация автообнаруженного видимого устройства без marker) корректно отражена в §7.2 (категория `on_plan`) и в AC3 — соответствует решению владельца в комментарии issue. - **Заявления о текущем поведении, использованные как база для «До», подтверждены кодом и канонoм**, а не додуманы: - `_bindingCandidates` в `src/houseplan-card.ts:14032` — существующий eligibility-код, который ТЗ предлагает извлечь в общий helper (§12); не выдумка. - `HaBindingStatus.kind` (`active`/`ha_disabled`/`orphaned`/`unverified`) в `src/ha-binding-status.ts:14-17` — точное совпадение с таблицей §7.3. - `marker.hidden`/`marker.removed` в `src/types.ts:118,121` — существуют, семантика совпадает с `docs/FILTERING.md`. - Жёсткий кап на 200 элементов в существующих списках кандидатов (`src/houseplan-card.ts:14106`, `19114` — `.slice(0, 200)`) — подтверждает описанную в §8.1/AC9 проблему, которую каталог обязан не унаследовать. - `duplicate_name_area` действительно устарела: `docs/FILTERING.md:145` прямо говорит «Duplicate names are still numbered», не скрываются. - `settings.new_device_ids` фильтруется от уже скрытых id при seed (`src/houseplan-card.ts:3679–3684`) — подтверждает §8.2 «Первично отфильтрованный скрытый кандидат не получает badge, как и сегодня». - Терминология «Добавить», «Скрытые и деактивированные», «Правила иконок» — точное совпадение с `src/i18n/ru.json:455-457` и `docs/USER-GUIDE.ru.md`. - **Технические предположения (§21)** промаркированы явно и корректно отделены от продуктовых решений — кроме пробела, описанного в M1. - **Не входит (§6)** корректно исключает #44 (discovery-настройки), #126 (смена area), #109 (multi-channel), bulk-операции, историю/графики, touch-паритет — всё по `docs/SCOPE.md` и `docs/TOUCH-SUPPORT.md`, без расширения скоупа. - **Откат (§19)** реалистичен: нет новых persisted-полей и версии модели, откат — вернуть старые кнопки; совместимость с созданными через «Скрыть из списка» обычными `hidden:true` маркерами сохраняется. - Названные тестовые артефакты существуют: `test/devices.test.mjs`, `test/ha-binding-status.test.mjs`, `test/device-presentation.test.mjs`, `test/device-presentation-policy.test.mjs`, `demo/smoke_hidden_flag.mjs`, `demo/smoke_binding_picker.mjs`, `scripts/smoke-select.mjs` — ни один не является выдумкой. ## Чего не проверял - Не запускал никакие гейты (`typecheck`/`test`/`build`/смоки) — на этапе ТЗ продуктового кода нет, это не применимо (§8 относится к код-ревью). - Не проверял `docs/ARCHITECTURE.md` и `docs/CANVAS.md` целиком постранично — только точечно то, что касается заявленных в ТЗ архитектурных решений (pure resolver, eligibility helper); полный аудит этих документов не требуется для ревью ТЗ. - Не оценивал производительность реализации (её ещё нет); проверил только, что заявленная асимптотика (§12, §17) не противоречит существующему коду (`.slice(0, 200)` факт капа подтверждён). - Не проверял golden/скриншот-инфраструктуру предметно — она релевантна коду, а не ТЗ. ## Вердикт Полностью выполненные формальные требования ТЗ (§7.1) не отменяют жёлтый вердикт: две находки Medium в скоупе (M1, M2) — реальные пробелы, оставить их «в тексте ревью» без правки запрещено §12 PROCESS.md. High-находок нет. **Вердикт: жёлтый.** ТЗ возвращается автору на правку M1 и M2; после правки — новый заход ревью по дельте (PROCESS.md §2.10).