Files
houseplan-card/docs/reviews/SPEC-REVIEW-485-r3.md
T
2026-09-09 03:50:50 +03:00

276 lines
23 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-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.
---
<!-- material-anchors: сгенерировано конвейером (#414) -->
## Материал раунда
- Ветка: `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
```