# SPEC-REVIEW-485-r3 Issue: [#485](https://github.com/Matysh/houseplan-card/issues/485). Материал: ветка `issue/485-radar-presence`, SHA `21950437b4343deead48c955d06191ea15bb567a` (baseline r2 материал `f4bab4a4bd5f4ac9b91cc16363985323a20ae907`, докс-only diff, проверено `git diff --stat` — только `docs/specs/485-radar-presence.md` и `docs/specs/485-radar-presence-stage1.md`; коммит `docs/reviews/SPEC-REVIEW-485-r2.md` самого документа r2 в дельту не входит, по прецеденту r2). Этап: **spec** (S4-spec-review). Заход: r3. Блокирующих циклов израсходовано 1 из 4 (зелёный вердикт r2 цикл не образовал и бюджет не потратил, #227). Трек: полный (метка `small` отсутствует). Ревьюер ≠ автор. ## Скоуп разбора (по дельте, PROCESS §2.9/§2.10, issue #214) Round r2 закрылся зелёным (High 0 / Medium 0) на материале `f4bab4a4`; findings предыдущего раунда закрывать нечего — этот раунд разбирает не исправление r2, а **новое уточнение владельца**, полученное уже после зелёного r2 (комментарий issue 2026-09-08T11:30:04Z): раздел «Присутствие на плане» в редакторе устройства показывается только подходящим устройствам, ранее настроенным радарам сохраняется путь исправления; общий переключатель не меняется. `git diff f4bab4a4..21950437 --stat` (без коммита самого документа r2): ``` docs/specs/485-radar-presence-stage1.md | 66 ++++++++++++++++++++++++++++++--- docs/specs/485-radar-presence.md | 15 ++++++-- ``` Формально дельта локальна по объёму (два файла, точечные абзацы в §2/§3 общего контракта и §2/§3/AC-таблицы этапа 1). Но по содержанию это не техническая правка и не декларативная фиксация ранее решённого — это **новый поведенческий контракт** (кто вообще видит точку входа в настройку радара), которого не было ни в r1, ни в r2. Согласно правилу §2.10 «разбор остаётся полным, если... смена контракта поведения» — я не ограничился построчной сверкой «что просил владелец / что дописано», а перечитал §3 обоих документов (модель источников/профилей) и точки входа в этапах 2–3, чтобы проверить, не рвёт ли новый гейт видимости то, что r1/r2 уже приняли как рабочее. Остальное содержимое пакета (модель идентичности, совместимость, i18n, перф, этапы 2–3 целиком) дельта не задевает — там разбор по дельте, без повторного прохода (см. «Унаследовано из r2»). ## Как проверялось - `gh issue view 485 --json labels,state` — метки `P2, feature, S4-spec-review`, `small` отсутствует: полный трек подтверждён, счётчик заходов/циклов совпадает с описанием задачи. - Построчный `git diff f4bab4a4..21950437` по обоим изменённым файлам (полный текст цитирую в разделе «Находки»). - Перечитано начало и конец тела issue #485 (`gh issue view 485 --json body`) — включая раздел «Что есть у easy-floorplan и что из этого брать» и его «Берём»/«Не берём» — источник профиля `range_v1`/«любой числовой сенсор». - Сверены точки входа в `docs/specs/485-radar-presence-stage2.md:75` и `docs/specs/485-radar-presence-stage3.md:86` («Device editor → Presence on plan → …») — обе описывают вход из уже определённого/настроенного радара, новому гейту видимости не противоречат. - Проверено техническое утверждение stage1 §2 про «existing conditional vacuum section» как прецедент: `src/houseplan-editor-runtime.ts` действительно содержит условный рендер `renderVacuumMapsSection` (строка 10701, внутри `... ? html\`... ${renderVacuumMapsSection(...)}\` : nothing`) — утверждение не выдумано. - `docs/UX-MODES.md` просмотрен на предмет канонического правила про показ/скрытие секций в редакторе устройства (в противовес «показывать, но дизейблить») — прямого правила, которое запрещало бы условный показ по типу устройства, не найдено; секция «пылесоса» — рабочий прецедент того же рода. - `docs/SCOPE.md` — дельта его не трогает; новый гейт видимости не расширяет и не сужает ранее записанное исключение (это вопрос UI-точки-входа в существующий редактор устройства, не новый класс данных/хранения). - Трейлеры коммита `21950437`: `Issue: #485`, `User-Visible: no` — верно для докс-only правки без видимого поведения. - Таблица i18n (§8 stage1) дельтой не тронута: новый гейт видимости не вводит собственного текста (ineligible-устройства просто не получают раздел), поэтому отсутствие нового ключа — не пропуск. ### Гейты Диапазон затрагивает только `docs/specs/*.md` (class C), продуктовый код и тесты не тронуты (`git diff --stat origin/dev..HEAD -- src custom_components` — пусто). Прогнаны сам, дёшево и напрямую по этому SHA: | Гейт | Команда | Результат | |---|---|---| | Markdown/diff-гигиена | `git diff --check f4bab4a4..HEAD -- docs/specs/485-radar-presence.md docs/specs/485-radar-presence-stage1.md` | чисто (exit 0) | | Документационные проверки | `node scripts/check-docs.mjs` | `Documentation checks passed (7 files, 12 external links)` | `tsc`/`test`/`build`/`invariants`/смоки/`golden`/`pytest` — не запускал: diff не касается `src/**`/`custom_components/**/*.py`, ни один AC этой стадии их не требует (эта же логика применялась в r1/r2 и подтверждена авторским `git diff --stat`). Отдельный зелёный Validate на этом SHA для данного раунда не искал — предыдущий раунд (r2, `f4bab4a4`) уже подтвердил репозиторий в целом, а эта дельта докс-only и не расширяет исполняемую поверхность. ## Закрытие раунда r2 r2 закрылся с High 0 / Medium 0 — закрывать нечего, таблица «находка → чем закрыта» неприменима. Материал этого раунда — не фикс находок r2, а новое уточнение владельца, полученное после зелёного вердикта (см. «Скоуп разбора»). ## Унаследовано из r2 Принято без повторной проверки в этом раунде, так как дельта их не задевает; основание — [SPEC-REVIEW-485-r2](https://github.com/Matysh/houseplan-card/blob/f4bab4a4bd5f4ac9b91cc16363985323a20ae907/docs/reviews/SPEC-REVIEW-485-r2.md) на SHA `f4bab4a4bd5f4ac9b91cc16363985323a20ae907`: - Продуктовая рамка, границы этапов 1→2→3 и их отнесение к J1/`docs/SCOPE.md`. - `docs/SCOPE.md`-исключения для производных HA-сущностей и записи/тепловой карты (закрытые в r1→r2) — эта дельта их не расширяет и не сужает. - Touch-декларация мастера калибровки этапа 1 (`best effort / intentionally degraded`) и этапов 2/3 — не тронута. - Единая модель идентичности (`source_generation`, `calibration_revision`, `server_session_id`/`seq`), лимиты, WS-контракт §7 stage1. - Полнота AC-таблиц C-1…C-8, S2-1…S2-22, S3-1…S3-18 и S1-1…S1-18 (кроме новой S1-19, разобранной в этом раунде отдельно). - Матрицы совместимости/жизненного цикла, i18n-таблица §8 stage1, перф-бюджеты §10 stage1. - Ответы Q1–Q4 и их количественное соответствие тексту. ## Находки ### M-1 (Medium, в скоупе) — новый гейт видимости, по тексту, исключает ранее принятый адаптер «любой числовой сенсор» (класс `range`), не оставляя описанного пути его настроить `docs/specs/485-radar-presence-stage1.md:55-59` (после дельты): > A positive presence-radar device/profile match or a verified radar-specific > source-role descriptor on the exact owning device makes it eligible. Match > evidence is structural adapter/registry metadata, not a friendly-name search. и `stage1.md:69-73`: > A normal light, switch, temperature/humidity sensor, ordinary PIR, virtual > marker, or a device with merely arbitrary numeric/binary entities is not a > radar candidate. Two numeric values or a generic occupancy/motion class alone > do not prove a radar profile. Оба условия — «структурные метаданные адаптера/реестра» и явное исключение «merely arbitrary numeric… entities» — требуют, чтобы устройство было **автоматически опознано** ещё до того, как раздел вообще появится. Ни здесь, ни где-либо ещё в дельте нет описанного пути «пользователь вручную объявляет устройство радаром», отличного от двух метаданных-путей. Это прямо противоречит собственному решению задачи, зафиксированному в теле issue #485 (раздел «Что есть у easy-floorplan и что из этого брать», подтверждено владельцем 07.09 вместе с остальным «Решение владельца»): > **Берём:** presence gate с fail-safe семантикой (сомнение → не показывать); > **«любой числовой сенсор» как низший общий знаменатель (класс «дальность»)**; > линия/дуга для одной оси; контур зоны только в редакторе; сглаживание между > отсчётами 1–4 Гц. и в самом контракте (`stage1.md:144-151`, таблица профилей, не тронута дельтой): > `range_v1` | One or two named distance channels, explicit length unit, optional > occupancy gate | Arc(s), not two people… `range_v1` — это буквально «любой числовой сенсор» из принятого решения: датчики класса Tuya ZY-M100/TS0601, Aqara FP1E, Linptech ES1, Seeed MR24 и любой самодельный узел, перечисленные в самой постановке issue (раздел «Что радары умеют отдавать»), никак не опознаются «структурными метаданными адаптера» — у них нет признанного дескриптора вроде LD2450. До этой дельты пользователь мог зайти в раздел устройства и вручную назначить источники (`range_v1`/`cartesian_v1`/ `polar_v1` — «generic explicit Cartesian/polar/range/occupancy inputs», common contract `485-radar-presence.md:86-88`, тоже не тронуто дельтой). После дельты раздел для такого устройства попадает под «merely arbitrary numeric… entities is not a radar candidate» и **не показывается вовсе** — то есть ручной путь входа, на котором держится вся «дальность»-ветка и вся идея «класса устройств, а не конкретной модели» (Суть issue: «Инструмент работы с любыми радарами присутствия… не конкретная модель, а класс устройств»), не имеет точки входа в UI. AC S1-19 (новая, `stage1.md:558`) требует протестировать в том числе «range»-радар как один из допущенных типов — то есть сам автор рассчитывает, что такой путь существует, но текст §2 его не описывает. **Почему это не мелочь, а не редактирование формулировки.** Уточнение владельца было одной строкой: «показывать только подходящим устройствам» — это про то, чтобы не засорять редактор лампочек и выключателей пустыми разделами. Реализация этой строки как «только структурно опознанные устройства» — решение автора, а не владельца, и оно тише удаляет из скоупа именно ту часть задачи (произвольный числовой датчик, взятый из easy-floorplan как «низший общий знаменатель»), которую до сих пор явно поддерживали r1 и r2. Это ровно тот случай из PROCESS §7.1: «поведение в пограничном случае» — граница между «опознанный радар» и «обычный датчик» — продуктовый вопрос, а не инженерный, и здесь он решён предположением, выданным за факт, без пометки «assumed, change freely» и без вопроса владельцу. **Как чинится (любой из вариантов, в скоупе задачи):** 1. Добавить в §2 явный ручной путь — например, действие «Это радар присутствия» на неопознанном устройстве, которое ведёт в тот же мастер с профилями `range_v1`/`cartesian_v1`/`polar_v1`, отдельное от двух автоматических путей и не открывающее раздел на самом деле неподходящих устройствах (лампах, выключателях) без явного действия пользователя; согласовать с «no friendly-name search» так, чтобы ручной путь не читался как автоматическое распознавание. 2. Либо явно сузить AC S1-19 и профильную таблицу так, чтобы `range_v1`/`cartesian_v1`/ `polar_v1` были помечены как доступные только через путь ремонта уже сохранённой конфигурации (т.е. фактически недостижимы для нового радара без признанного дескриптора), и одним пакетным вопросом с вариантом по умолчанию спросить владельца, допустимо ли это сужение — вопрос ровно того типа, который PROCESS §7.1 адресует владельцу («relates to what a person sees or does», здесь — сможет ли человек вообще настроить нераспознанный радар). Находка в скоупе задачи (правится в этом же ТЗ, отдельный issue не заводится, #202). ## Что проверено и корректно - Правило скрытия для обычных устройств (лампы/выключатели/температура/обычный PIR/виртуальный маркер) и сохранение пути ремонта для уже настроенного радара (в том числе при `unavailable`/неподдерживаемой версии) — сформулировано недвусмысленно, с исчерпывающим списком случаев и негативными свидетелями в AC S1-19 (`ordinary-device header assertion`, `offline/range/manual repair assertion`, `empty-installation settings assertion`). - Общий переключатель «Показывать присутствие на плане» остаётся видимым без радаров и не зависит от gate — текст (`stage1.md:108-110`, `485-radar-presence.md:69-71`) дословно совпадает с формулировкой уточнения владельца в issue. - Точки входа этапов 2 и 3 (`stage2.md:75`, `stage3.md:86`) остаются валидны при новом гейте: обе описывают вход из уже определённого/настроенного радара, не из произвольного устройства. - Новых i18n-ключей дельта не требует и не добавляет — раздел на неподходящих устройствах просто отсутствует, не превращается в текст с пустым состоянием. - Техническое утверждение про существующий условный раздел пылесоса как прецедент подтверждено чтением `src/houseplan-editor-runtime.ts:10701` — не выдумано. - `docs/SCOPE.md` не требует правки для этой дельты: вопрос — точка входа в существующий редактор устройства, не новый класс данных/хранения. - Трейлеры коммита корректны; дельта не касается `src/**`/`custom_components/**/*.py`. ## Чего не проверял - Не перепроверял AC-таблицы C-1…C-8, S2-1…S2-22, S3-1…S3-18 и S1-1…S1-18 (кроме новой S1-19), модель идентичности, совместимость, перф-бюджеты, WS-контракт — дельта их не задевает; раздел «Унаследовано из r2» перечисляет, что принято без повторной проверки. - Не запускал `npx tsc --noEmit`, `npm test`, `npm run build`, `npm run invariants`, браузерные смоки, `golden:verify`, `python -m pytest tests_backend` — diff докс-only, не касается `src/**`/`custom_components/**/*.py`, ни один AC пакета не требует их прогона на стадии spec-review. - Не проверял `docs/USER-GUIDE.ru.md`/`docs/USER-GUIDE.md` построчно на предмет терминологии — фича не выпущена, руководство ещё не содержит раздела о радарах (`grep` по обоим файлам — без совпадений), сверять не с чем. - Не оценивал остальные два варианта устранения M-1 на предмет объёма следующего раунда — это решение автора и, при необходимости, владельца. ## Вердикт **Жёлтый.** High — 0, Medium — 1 (M-1, в скоупе задачи, чинится в этом же issue, #202). Находка блокирует переход S4 → S5: новый гейт видимости, введённый этой дельтой, тише сужает ранее принятое решение «любой числовой сенсор — класс `range`» без пометки предположения и без вопроса владельцу. Остальная часть пакета (унаследованная из r2) находок не имеет. Этот жёлтый вердикт образует цикл и расходует бюджет §4: было израсходовано 1 из 4, после этого раунда — 2 из 4. --- ## Материал раунда - Ветка: `issue/485-radar-presence`, коммит `21950437b434` — ребейз его осиротит, и это нормально: ниже якоря, которые ребейз не меняет. - Дерево материала: `717b51fb35f3c98731eb72e53d0cc2ab87db25d4` ``` git log --all --format='%H %T' | grep 717b51fb35f3 ``` - ТЗ `docs/specs/485-radar-presence-stage1.md`, блоб `c5d9960cfaa0fa5573e58d384e8e2b7eeddc6fcc` ``` git log --all --find-object=c5d9960cfaa0fa5573e58d384e8e2b7eeddc6fcc -- docs/specs/485-radar-presence-stage1.md ``` - ТЗ `docs/specs/485-radar-presence-stage2.md`, блоб `b403e73fc26ba0ffb97be4ffa73be067d179880f` ``` git log --all --find-object=b403e73fc26ba0ffb97be4ffa73be067d179880f -- docs/specs/485-radar-presence-stage2.md ``` - ТЗ `docs/specs/485-radar-presence-stage3.md`, блоб `c65e28389d762216c47f00249bdb751f3c9d5082` ``` git log --all --find-object=c65e28389d762216c47f00249bdb751f3c9d5082 -- docs/specs/485-radar-presence-stage3.md ``` - ТЗ `docs/specs/485-radar-presence.md`, блоб `dfc1edc3aa10d19c16eb37e1ddacf7c81c439216` ``` git log --all --find-object=dfc1edc3aa10d19c16eb37e1ddacf7c81c439216 -- docs/specs/485-radar-presence.md ```