docs: #44 spec revision 2 — discovery filters UI, preview, excluded-integration reason

User-Visible: no
Issue: #44
This commit is contained in:
Codex
2026-08-30 13:29:57 +03:00
parent 8819e390c9
commit 3d4c509088
+165 -41
View File
@@ -1,59 +1,183 @@
# ТЗ #44 — Явные фильтры и группировка устройств
- Issue: https://github.com/Matysh/houseplan-card/issues/44
- Приоритет: P2
- Статус ТЗ: policy proposal ready for owner review
- Связано: device inbox #29, field registry #33
- Приоритет: P2, tech-debt; полный трек (discovery/UI/settings/i18n/tests —
решение аналитики 2026-08-15)
- Ревизия: 2 (2026-08-30) — policy v1 принята владельцем (вход в работу без
возражений к записанной политике); конкретизация под инбокс v1.69
- Связано: device inbox #29, field registry/манифест #33 (паспорта
`allow-extra` обоих ключей уже выданы), персист-паттерн #377
## Цель
## Сценарий
Убрать скрытые настройки, влияющие на discovery. Каждый ключ либо получает
поддерживаемый advanced UI и documented default, либо мигрирует в фиксированное
product rule.
Хозяин плана открывает «Устройства» и на вкладке «Доступные» видит новый
раскрываемый блок «Фильтры обнаружения». Там два честных элемента: тумблер
«Объединять светильники комнаты в группу» (включён по умолчанию — как продукт
и вёл себя всегда) и список «Исключённые интеграции» с рекомендованным
набором, поиском по реально присутствующим в HA интеграциям и кнопкой
«Вернуть рекомендуемые». Изменение показывает счётчики «появится / скроется /
сгруппируется» до сохранения; Сохранить пишет настройки один раз.
## Решение v1
## Что человек увидит до и после
Оба текущих ключа остаются поддерживаемыми и становятся видимыми в Advanced
разделе inbox:
**До**: группировка света и исключение интеграций происходят молча; понять
«почему этого устройства нет в списке» нельзя. **После**: обе настройки видны
и объяснимы; у кандидата, скрытого фильтром, в инбоксе появляется причина
«Исключена интеграция X»; ничего не меняется само — только по Сохранить.
### `group_lights`
## Проблема
- default `true`;
- label: «Объединять несколько светильников комнаты»;
- preview показывает, какие bindings будут группой/отдельными marker;
- переключение не применяется до Save и не удаляет explicit markers;
- existing light-group marker сохраняет lifecycle; конфликт bindings preview.
Оба ключа — «третий вариант», запрещённый issue: влияют на результат, не
видны, без migration policy. Факты кода: `group_lights` читается как
`!== false` (default TRUE, `devices.ts:1033/1098`); `exclude_integrations`
при наличии ЦЕЛИКОМ подменяет продуктовый `EXCLUDED_DOMAINS` (13 доменов,
`rules.ts:12`; потребители `houseplan-card.ts:3924`, `space-render.ts:245`,
discovery `devices.ts:1491`).
### `exclude_integrations`
## Решение (policy v1, принята)
- default — текущий `EXCLUDED_DOMAINS`/product list;
- UI — searchable multi-select интеграций, реально присутствующих в registry,
плюс reset to recommended defaults;
- изменение влияет только на automatic candidates/seed, не скрывает explicit
live marker и не удаляет tombstone;
- reason в inbox — «Исключена интеграция X».
Оба ключа — поддерживаемые advanced-настройки. Storage-имена и семантика НЕ
меняются (обратная совместимость): `group_lights` отсутствие = true;
`exclude_integrations` отсутствие = продуктовый список, наличие = полная
замена. UI прячет эту механику за честными представлениями.
Название storage key `exclude_integrations` сохраняется для compatibility;
семантика и default фиксируются в registry #33.
## Скоуп
## Исследование перед включением Save
### 1. Блок «Фильтры обнаружения» в инбоксе
Локальный config-audit считает значения/отклонения от defaults без вывода ids.
Synthetic fixtures моделируют реальные классы: group lights, parent device,
service/bridge, explicit override, tombstone. Если audit показывает, что ключ
невозможно объяснить без вредного поведения, owner может отдельным решением
перевести его в fixed rule до реализации UI.
- Раскрываемая секция внизу вкладки «Доступные» диалога «Устройства»
(editor-runtime; холодный View не задет — весь UI в runtime, класс #357).
- **Тумблер группировки**: отражает `settings.group_lights` (unset→вкл).
Выключение НЕ удаляет существующие маркеры (в т.ч. group-маркеры) — меняет
только будущих кандидатов discovery.
- **Исключённые интеграции**: чипы текущего действующего набора (ключ или
продуктовый список); поиск-добавление по интеграциям, реально
присутствующим в registry HA (platform-ы entity registry); удаление чипа;
кнопка «Вернуть рекомендуемые» (= удалить ключ из settings). Если
действующий набор равен продуктовому — ключ в settings отсутствует
(паттерн «дефолт = отсутствие ключа», #377).
- **Превью до сохранения**: счётчики «появится N / скроется M /
группировка изменит K» — пересчёт кандидатов discovery на черновых
значениях (чистая функция buildDevices уже принимает settings/excluded
через ctx — используется она же, без дублирования логики). Список
затронутых биндингов раскрывается (имена, без лишних id).
- **Транзакция**: Изменения живут в черновике диалога; Сохранить пишет
settings ОДИН раз штатным сериализованным путём (expected_rev #340);
Отмена — no-op. Undo — штатный именованный снапшот конфига (паттерн
существующих правок настроек).
## UX/transaction
### 2. Причина «Исключена интеграция X» в инбоксе
Preview diff: новые/скрытые/grouped candidates counts и конкретный список в
локальной session. Apply пишет settings один раз, rebuild devices и создаёт
named undo/config snapshot. Cancel no-op. Два клиента используют config rev.
Кандидат, отфильтрованный исключением, при показе на вкладке «Скрытые»
несёт `DeviceInboxReason` нового значения `excluded_integration` с текстом
«Исключена интеграция {integration}». (Сегодня такие устройства не попадают
в инбокс вовсе — они появляются на «Скрытых» именно с этой причиной;
объём выдачи ограничен существующей механикой вкладки.)
## Приёмка
### 3. Registry/доки
- ни один runtime discovery key не скрыт от пользователя/registry;
- defaults совпадают frontend, backend, docs и fixtures;
- explicit marker никогда не исчезает из-за filter change;
- preview объясняет каждую изменившуюся binding;
- old config даёт прежний result до user action.
- `config-field-registry.mjs`: статусы обоих ключей `decision-required` →
`current`, `ui: 'device inbox → Discovery filters'`, `enforcedBy` — новый
смок; migration-строки — «none, supported setting».
- USER-GUIDE(.ru): подраздел «Фильтры обнаружения» в теме инбокса.
## Не-скоуп
- Изменение семантики replacement→additive у `exclude_integrations`
(совместимость; additive-режим — отдельное решение, если понадобится).
- Пер-пространственные фильтры; фильтры по доменам сущностей.
- Изменение самого продуктового списка `EXCLUDED_DOMAINS`.
- Материализация/`show_all` (закрыто ранее, #33 registry).
## Контракт поведения
1. Конфиг без ключей → поведение байт-в-байт сегодняшнее (default-путь не
меняется ни в одном потребителе).
2. Explicit-маркер (живой или tombstone) НИКОГДА не исчезает из-за смены
фильтров — фильтры влияют только на автоматических кандидатов и seed.
3. Оба значения читаются из одного источника (`_settings`) всеми
потребителями — новых копий состояния нет.
4. `Вернуть рекомендуемые` удаляет ключ (не пишет копию списка).
5. Никакой записи до явного Сохранить; двойная вкладка — штатный conflict.
## UX / i18n
Новые ключи (en/ru/de/fr, все четыре — паритет-гейт): заголовок секции,
label+hint тумблера, label списка, placeholder поиска, кнопка reset,
счётчики превью (3), причина `device_inbox.reason_excluded_integration`
(с плейсхолдером {integration}). Порядка 9 ключей.
## Модель данных и миграция
Нет изменений хранения: ключи те же, семантика та же. Бэкенд-схему не
трогаем: ключи остаются `allow-extra` (паспорта #33). Причина: перенос в
строгую схему — отдельное решение с рисками отклонения старых конфигов;
issue требует видимости и policy, не пере-схемизации.
## Критерии приёмки
- **AC1** (смок): тумблер группировки выключить+Сохранить → в конфиге
`group_lights: false`, повторное открытие показывает выкл, discovery-
кандидаты содержат отдельные лампы вместо группы; обратное включение →
ключ УДАЛЯЕТСЯ из settings (default = отсутствие).
- **AC2** (смок): добавление интеграции в исключения → превью-счётчик
«скроется» ненулевой; Сохранить → ключ в settings = полный действующий
список; «Вернуть рекомендуемые» + Сохранить → ключа нет.
- **AC3** (смок): у явного маркера с binding исключённой интеграции ничего
не меняется (живой остаётся на плане, tombstone не трогается).
- **AC4** (смок): скрытый фильтром кандидат виден на «Скрытых» с причиной
«Исключена интеграция X».
- **AC5** (юнит): резолвер действующего набора: unset→продуктовый список,
[]→пусто (валидная «ничего не исключать»), список→список; тумблер:
unset→true.
- **AC6** (юнит): превью-диффер чист: те же ctx-входы, что у boевого
buildDevices (контракт-проверка, что превью не дублирует логику фильтра).
- **AC7**: отсутствие ключей → discovery-выдача байт-в-байт с сегодняшней
(регресс-юнит на фикстуре).
- **AC8**: полный гейт; i18n 4/4; бюджет: UI в editor-runtime (lazy),
initial ≈ без изменений.
## План автотестов
- Юниты: резолверы (AC5), превью-контракт (AC6), регресс дефолта (AC7) —
test/devices.test.mjs + test/device-inbox.test.mjs.
- Смок `demo/smoke_discovery_filters.mjs` (новый): AC1–AC4 на демо-стенде
с моковым config/set (паттерн #377-смока).
- Мутанты: м1 — превью-счётчики от копии логики (подмена вызова общего
билдера константой) → красный AC6/смок; м2 — «Вернуть рекомендуемые»
пишет копию списка вместо удаления ключа → красный AC2.
## Риски
- Пересчёт превью на большом registry — переиспользуется кэшируемый
buildDevices; расчёт по явному действию (открытие секции/изменение), не
на каждый тик.
- Реason-вкладка «Скрытые» получает новый класс записей — объём ограничен
существующим механизмом вкладки; смок проверяет отсутствие дублей.
- Гонка вкладок — штатный conflict #340.
## Откат
`git revert`: UI и причина исчезают, ключи продолжают работать как до
задачи (скрыто); записанные пользователями значения остаются валидными —
семантика не менялась. Потери данных нет.
**DoR-примечания:** миграция/compatibility — нет (семантика и имена ключей
неизменны); touch — стандартные элементы диалога (чипы/тумблер), новых
жестов нет; производительность — превью по явному действию.
## Release-артефакты
- CHANGELOG + CHANGELOG.ru: user-visible запись (настройки фильтров стали
видимыми и объяснимыми), ссылка #44.
- USER-GUIDE(.ru): подраздел; ARCHITECTURE.md: абзац про policy «нет скрытых
discovery-ключей» со ссылкой на registry.
## Принятые предположения
- Размещение — вкладка «Доступные» (фильтры определяют её содержимое);
если ревью ТЗ предпочтёт отдельную вкладку/шестерёнку — правится без
смены механики.
- `[]` (пустой список) — валидное значение «ничего не исключать», отличное
от отсутствия ключа; так уже работает код (`list ? new Set(list) : EXCLUDED_DOMAINS`).
- Причина показывается на «Скрытых», НЕ в отдельной новой вкладке.