docs: update device inbox lifecycle spec

Issue: #29
User-Visible: no
This commit is contained in:
Matysh
2026-08-27 23:05:45 +00:00
committed by claude[bot]
parent 2b1964f973
commit fcf30073e9
+516 -47
View File
@@ -1,65 +1,534 @@
# ТЗ #29 — Inbox и объяснимый жизненный цикл устройств
# ТЗ #29 — единый каталог и жизненный цикл устройств
- Issue: https://github.com/Matysh/houseplan-card/issues/29
- Приоритет: P1
- Статус ТЗ: draft, требуется UX-утверждение
- Связано: #44 переносит advanced-фильтры в этот интерфейс
- Тип: feature
- Трек: обычный
- Связано: #44 (настройки discovery), #109 (поиск каналов устройства),
#126 (смена HA area), #262 (возврат exact entity после удаления device)
## Цель
## 1. Сценарий
Заменить разрозненные «Добавить»/«Показать скрытые» одним read-only при
открытии каталогом, который объясняет состояние каждой HA-привязки и даёт
явные действия жизненного цикла.
**Персона:** администратор дома из `docs/SCOPE.md`.
## Единая классификация
**Поверхность:** полный `houseplan-card`, desktop-first редактор устройств.
Pure resolver получает registry snapshot, live states, markers и product
filters и возвращает одну запись на canonical binding:
**Момент:** пользователь добавил, скрыл или удалил устройства, Home Assistant
обнаружил новые или деактивировал существующие, и теперь нужно понять, какие
объекты представлены на плане и что с каждым из них можно сделать.
| Раздел | Условие | Основные действия |
## 2. Что человек увидит до и после
**До:** в панели отдельно находятся «Добавить» и «Скрытые и
деактивированные»; они позволяют выполнить операцию, но не дают единой картины
и не объясняют, почему конкретного устройства нет на плане.
**После:** одна кнопка «Устройства» открывает каталог всех относящихся к плану
HA-привязок с поиском, понятной категорией, причиной текущего состояния и
доступным следующим действием; автоматически появившееся устройство уже
считается находящимся на плане и отдельно помечается как новое.
## 3. Проблема
Текущая реализация уже поддерживает несколько устойчивых состояний:
- автоматически отображаемое устройство без сохранённого `marker`;
- явный видимый marker;
- пользовательски или автоматически скрытый `hidden:true` marker;
- временно неактивную HA-привязку (`ha_disabled`, `orphaned`, `unverified`);
- удалённую exact binding с `removed:true`, которую можно добавить заново;
- отдельную `entity:X`, возвращённую из-под tombstone родителя `device:D`.
Эти состояния обслуживаются разными UI-путями и не образуют объяснимого
каталога. Пользователь вынужден помнить разницу между скрытием, удалением,
деактивацией в HA, фильтрацией и автоматическим появлением. В результате
одинаковое отсутствие значка визуально выглядит как несколько разных причин,
а поиск уже размещённого устройства возможен только глазами на плане.
Старое ТЗ от 9 августа дополнительно считало любую binding без marker «новой».
Это расходится с текущим zero-config поведением: автообнаруженное устройство в
привязанной HA area уже рисуется без marker.
## 4. Цель и метрики результата
Создать одну read-only при открытии проекцию lifecycle, которая использует
существующие данные и действия, а не вводит второй источник истины.
Результат считается достигнутым, если:
1. для каждой включённой в каталог exact binding существует ровно одна базовая
категория;
2. каталог отличает пользовательское намерение от временного статуса HA;
3. открытие, поиск, смена вкладки и просмотр причины не меняют config, layout и
их revisions;
4. все существующие операции — найти, настроить, добавить, показать и добавить
заново — доступны из одной поверхности;
5. текущая автоматическая раскладка, tombstones, exact entity ownership и
фильтрация не меняют семантику.
## 5. Скоуп
Входит:
- единый каталог в редакторе устройств;
- pure resolver строк каталога;
- категории, статусы, причины, счётчики и поиск;
- действия над одной строкой;
- возврат из существующего диалога устройства обратно в каталог;
- локальное выделение найденного marker на плане;
- desktop, узкая responsive-компоновка без горизонтального скролла, keyboard
navigation;
- RU/EN i18n, unit/smoke/golden, пользовательская и каноническая документация.
## 6. Не входит
- изменение правил автоматического discovery, группировки света и списка
исключённых интеграций — это #44;
- управление HA registry: активация, деактивация, переименование или смена area;
- автоматический перенос marker при смене HA area — это #126;
- исправление отсутствующих каналов конкретного multi-channel устройства — #109;
- bulk hide/show/delete/add;
- история устройств, графики и статистика;
- добавление виртуальных markers в lifecycle-каталог: они не имеют HA binding и
остаются редактируемыми кликом на плане;
- гарантированная полнофункциональность редактора на touch по
`docs/TOUCH-SUPPORT.md`.
## 7. Термины и две независимые оси
### 7.1 Exact binding
Ключ строки — точная привязка `device:<device_id>` либо
`entity:<entity_id>`. `device:D` и `entity:X`, принадлежащая D, являются разными
ключами и могут осознанно сосуществовать. Семейство HA device не является
единицей дедупликации.
### 7.2 Базовая категория — пользовательское намерение
| Категория | Условие | Смысл |
|---|---|---|
| Новые | активная допустимая binding без marker/tombstone | Добавить, скрыть |
| На плане | live marker, `hidden !== true` | Найти, редактировать |
| Скрытые | live marker `hidden:true` либо seed-кандидат filter | Показать/добавить, причина |
| Доступные снова | tombstone `removed:true`, binding снова существует | Добавить заново |
| `on_plan` — «На плане» | Binding представлена runtime-устройством на плане либо живым marker с `hidden !== true` | Объект уже принадлежит плану. Отсутствие сохранённого marker не делает автоустройство «не добавленным» |
| `available` — «Доступны» | Активная допустимая binding не представлена runtime-устройством, живым marker или tombstone | Её можно добавить впервые |
| `hidden` — «Скрытые» | Живой marker имеет `hidden:true` | Объект сохранён, но пользовательское намерение — не показывать его |
| `readd` — «Доступны снова» | Exact binding имеет `removed:true` и её существование сейчас подтверждено | Объект был удалён с плана и может быть создан заново |
HA-disabled binding остаётся в своём lifecycle-разделе, но получает статус
«Отключено в Home Assistant» и недоступное действие показа согласно текущему
контракту disabled devices. Orphaned сохранённый marker остаётся «На плане» или
«Скрытые» с предупреждением, а не превращается в «Новый».
Приоритет категорий для одного exact key:
## Причины
`removed → hidden → on_plan → available`.
Причина — stable enum, локализованный в UI: `manual_hidden`, `ha_disabled`,
`service_entry`, `excluded_integration`, `excluded_domain`, `grouped_light`,
`represented_by_parent`, `duplicate_name_area`, `removed`, `orphaned`,
`limited_registry`. Regex/id могут быть в раскрываемой диагностике, но не в
основной фразе.
Живой exact marker перекрывает tombstone той же exact binding. Tombstone
родителя `device:D` не перекрывает живую `entity:X`, и наоборот, согласно #262.
## UX
### 7.3 Операционный статус — состояние Home Assistant
- Кнопка редактора устройств открывает wide `hp-dialog`/side sheet с поиском,
tabs/filters и счётчиками.
- Просмотр, поиск и раскрытие причины ничего не пишут в config.
- «Найти» переключает пространство, закрывает inbox и мягко выделяет marker.
- «Добавить» открывает существующий device dialog с preselected binding.
- «Скрыть» материализует `hidden:true`, но не `removed:true`.
- «Добавить заново» заменяет tombstone одним live marker и не наследует старую
позицию, файлы или trail, уже удалённые подтверждённым Delete.
- Все mutation actions получают Undo/confirmation согласно их текущей
семантике; bulk actions в v1 не входят.
Статус накладывается на категорию и не переносит строку между вкладками:
## Инварианты и конкуренция
| Статус | Источник | Поведение |
|---|---|---|
| `active` | `HaBindingStatus.kind === 'active'` либо положительное доказательство для нового кандидата | Доступны обычные действия категории |
| `ha_disabled` | Авторитетный registry подтверждает деактивацию | Строка помечена «Отключено в Home Assistant»; показать, добавить и выполнить Find нельзя |
| `orphaned` | Авторитетный registry подтверждает отсутствие сохранённой binding | Строка сохраняет свою базовую категорию, получает предупреждение и безопасные действия Edit/Delete |
| `unverified` | Registry недоступен или ограничен | Нельзя делать отрицательный вывод; строка не объявляется удалённой или деактивированной |
Canonical binding уникальна. Повторный save и конфликт revision не создают
дубликат. Registry refresh обновляет список с сохранением tab/search, но не
закрывает редактируемый dialog. Limited registry не выводит ложный «удалён».
Пример: сохранённый видимый marker, который позже деактивировали в HA, остаётся
в «На плане», но строка сообщает, что сейчас он временно не показывается.
`hidden:true` + HA-disabled остаётся в «Скрытые» и показывает обе причины.
## Проверки и приёмка
## 8. Состав каталога
- матрица resolver по marker/hidden/removed/disabled/orphaned/filter;
- два клиента и revision conflict;
- re-add device/entity tombstones, virtual marker вне inbox;
- поиск, keyboard navigation, narrow layout и ru/en golden;
- любой кандидат находится ровно в одном разделе и имеет понятную причину;
- открытие inbox не меняет config/layout/revisions.
Pure resolver строит детерминированный snapshot из:
- runtime `_devices`;
- `config.markers`;
- `settings.new_device_ids`;
- авторитетного или ограниченного HA registry snapshot;
- live states и текущей проекции доступности;
- действующих product filters и synthetic light groups;
- связей HA area → пространство/комната.
Кандидаты собираются из того же источника и по тем же eligibility-правилам,
что текущий Add picker. Нельзя поддерживать отдельную, постепенно расходящуюся
копию правил.
Виртуальные markers исключаются. Tombstone без положительного доказательства,
что exact binding снова существует, не показывается как «Доступно снова».
Limited registry никогда не превращает неизвестность в `ha_disabled`,
`orphaned` или `readd`.
### 8.1 Entity-уровень
- Уже размещённые `entity:*` markers всегда присутствуют в каталоге.
- В категории «Доступны» individual child entities показываются после включения
фильтра **«Показывать сущности»**, как в текущем Add dialog.
- Поиск выполняется по полному индексу и может найти exact entity независимо от
её позиции после первых 200 элементов; hard cap до фильтрации запрещён.
- Явные `device:D` и `entity:X` не скрывают друг друга из каталога. Residual
automatic parent продолжает строиться по текущему контракту `buildDevices`.
### 8.2 Признак «Новое»
`settings.new_device_ids` остаётся единственным серверным источником этого
признака.
- Автоматически появившееся и уже видимое устройство находится в «На плане» и
получает badge/фильтр **«Новое»**.
- Открытие каталога и действие Find не подтверждают новизну.
- Открытие существующего диалога настройки через Edit подтверждает новизну по
текущему контракту.
- Первично отфильтрованный скрытый кандидат не получает badge, как и сегодня.
## 9. Причины и объяснения
Строка может иметь одну основную lifecycle-причину и дополнительные статусы.
В UI используются локализованные сообщения, не внутренние regex или raw id.
Stable reason enum:
- `visible_auto` — найдено автоматически и уже находится на плане;
- `visible_explicit` — добавлено или настроено явно;
- `manual_hidden` — скрыто пользователем;
- `automatic_hidden` — скрыто автоматически, точная историческая причина не
доказуема;
- `service_entry`;
- `excluded_integration`;
- `excluded_domain`;
- `grouped_light` / `represented_by_parent`;
- `removed`;
- `no_bound_room` — доступно, но HA area не связана с комнатой плана;
- `ha_disabled_device`, `ha_disabled_entity`, `ha_disabled_all_entities`;
- `orphaned_device`, `orphaned_entity`;
- `registry_unavailable`.
`duplicate_name_area` удаляется из старого ТЗ: совпадающие имена сейчас
нумеруются, а не скрываются.
Если точную причину старого seed-marker нельзя доказать по текущему registry и
правилам, показывается честное **«Скрыто автоматически»**. Новое поле с
исторической причиной в config не добавляется.
Raw binding и диагностические детали допускаются в раскрываемой строке
«Технические сведения» и копируются отдельно; они не заменяют пользовательское
объяснение.
## 10. UX
### 10.1 Панель редактора
- Кнопки **«Добавить»** и **«Скрытые и деактивированные»** заменяются одной
кнопкой **«Устройства»** с иконкой списка/устройств.
- **«Правила иконок»** остаётся отдельной кнопкой.
- Скрытые markers больше не рисуются поверх плана постоянным локальным режимом:
доступ к ним даёт каталог. Это устраняет состояние панели, неочевидное после
возврата в редактор.
### 10.2 Диалог
Используется wide `hp-dialog` с заголовком **«Устройства на плане»**.
Верхняя область:
1. поле поиска с initial focus;
2. действие **«Добавить виртуальное устройство»**;
3. вкладки/фильтры со счётчиками:
- **На плане**;
- **Доступны**;
- **Скрытые**;
- **Доступны снова**;
4. дополнительный фильтр **«Новые»** для «На плане»;
5. **«Показывать сущности»** для «Доступны».
Строка содержит:
- текущую иконку;
- пользовательское имя;
- тип «Устройство»/«Сущность»;
- пространство и комнату, если известны;
- интеграцию/модель, если известны;
- badge «Новое» и operational status;
- одну короткую причину;
- primary action и меню дополнительных действий.
Поиск регистронезависим и работает по имени, модели, интеграции, названию
пространства/комнаты, `entity_id` и exact binding. Сначала фильтруется полный
snapshot, затем применяется progressive rendering. Первые 100 строк выводятся
сразу, остальные — кнопкой «Показать ещё» порциями по 100; счётчики всегда
относятся к полному результату.
Пустое состояние каждой вкладки объясняет, что сюда попадает и какое действие
может изменить результат.
### 10.3 Действия строк
| Категория/статус | Primary | Дополнительные действия |
|---|---|---|
| На плане, реально отображается | **Найти на плане** | Настроить, Скрыть |
| На плане, временно не отображается из-за HA status | **Настроить** | Открыть в HA, Удалить через существующий dialog |
| Скрытые, active | **Показать** | Настроить |
| Скрытые, HA-disabled | **Настроить** | Открыть в HA; «Показать» disabled с объяснением |
| Доступны | **Добавить** | Скрыть из списка |
| Доступны снова | **Добавить заново** | — |
Hide/Show — обратимые одиночные действия без подтверждения, как текущий флаг
скрытия. Delete остаётся только в существующем диалоге и сохраняет текущее
подтверждение. Add/Re-add не сохраняют объект немедленно: они открывают
существующий device dialog с preselected exact binding; Cancel ничего не пишет.
«Скрыть из списка» материализует обычный `hidden:true` exact marker и переносит
строку в «Скрытые». Это не tombstone и всегда обратимо.
### 10.4 Find и возврат из настройки
Find:
- доступен только если marker реально отрисовывается;
- переключает пространство при необходимости;
- закрывает каталог;
- перемещает viewport так, чтобы marker оказался внутри безопасной центральной
области, не меняя zoom без необходимости;
- выделяет marker существующим selection style и дополнительным конечным
акцентом не дольше 1,5 секунды;
- не открывает карточку, не выполняет tap action и не подтверждает badge «Новое».
Add/Edit/Show через существующий device dialog временно заменяют каталог.
Cancel или Save возвращают пользователя в тот же tab/search/filter и к той же
логической строке; snapshot пересчитывается. После удаления открывается
обновлённая категория «Доступны снова», если binding подтверждена HA.
Registry/config refresh при открытом каталоге обновляет строки и счётчики, но
не сбрасывает tab/search/filter. Открытый device dialog не закрывается из-за
refresh; результат применяется после возврата.
## 11. Модель данных, сохранение и конкуренция
Новых persisted-полей и версии модели нет.
- Категория, статус, reason и состояние UI вычисляются в памяти.
- Tab, search, entity/new filters, progressive limit, scroll anchor и
`returnToInbox` — локальное состояние текущего экземпляра карточки и не
переживают reload.
- Hide/Show используют существующий `marker.hidden`.
- Delete/Re-add используют существующий `marker.removed` и текущую очистку
layout/files/trails.
- New badge использует `settings.new_device_ids` без смены формата.
- Любая mutation проходит существующий config revision/expected-rev путь.
Конфликт не создаёт дубликат: карточка перечитывает snapshot, сохраняет
tab/search и показывает локализованный toast.
- Открытие каталога, Find, поиск, смена вкладки, раскрытие причины и Show more не
вызывают `_saveConfig`, layout update или acknowledgement.
Существующий `filter_seeded` остаётся compatibility-механизмом. Если старый
config материализуется при входе редактирующего клиента, это отдельная
идемпотентная миграция текущего продукта; каталог не инициирует и не маскирует
её как пользовательское действие.
## 12. Архитектурный контракт
Новый pure-модуль (рабочее имя `src/device-inbox.ts`) владеет:
- `DeviceInboxRow`, category/status/reason enums;
- сборкой и дедупликацией exact bindings;
- capability flags действий;
- поисковым индексом и стабильной сортировкой.
Он не импортирует Lit, не пишет config и не выполняет service calls.
Eligibility новых bindings обязана использовать общий helper текущего Add
picker. В ходе реализации общая логика извлекается из
`houseplan-card.ts::_bindingCandidates`; две независимые реализации запрещены.
Стабильная сортировка внутри вкладки:
1. `new`;
2. blocking operational status;
3. localized display name;
4. exact binding как tie-breaker.
Runtime complexity: O(devices + entities + markers) на snapshot плюс O(rows)
на поиск. Нельзя для каждой строки повторно обходить все registry entities.
Результат memoize по config epoch, registry revision, device roster signature,
new ids и UI entity filter.
## 13. Доступность и responsive
- `hp-dialog` сохраняет focus trap и restore focus.
- Search получает initial focus; Escape закрывает dialog.
- Tabs имеют `role=tablist`, выбранная вкладка — `aria-selected`; стрелки
переключают вкладки.
- Строка и каждое действие достижимы Tab; icon-only действия имеют aria-label.
- Изменение результатов поиска объявляется ненавязчивым `aria-live=polite`.
- На узкой ширине метаданные переносятся под имя, действия не создают
горизонтальный скролл.
- View/киоск не меняются. Редактор на touch остаётся best effort; каталог не
должен ломать pinch/pan View и не добавляет gestures на сам план.
## 14. i18n
Новые ключи создаются одновременно в `src/i18n/en.json` и
`src/i18n/ru.json` для:
- кнопки и заголовка каталога;
- четырёх вкладок, счётчиков и empty states;
- search, Show entities, New, Show more;
- Add virtual, Find, Edit, Hide, Show, Hide from list, Add, Re-add;
- всех reason/status из §9;
- unavailable action hints, refresh/conflict toast и accessibility labels.
Имена integration/model/binding не переводятся. Формулировки интерфейса
синхронизируются с RU/EN user guide.
## 15. Критерии приёмки
### AC1 — единая точка входа (golden + smoke)
В Device editor вместо двух кнопок Add/Show hidden отображается одна кнопка
«Устройства»; она открывает каталог с четырьмя категориями, поиском и
счётчиками. «Правила иконок» остаётся доступной.
### AC2 — детерминированная классификация (unit)
Матрица покрывает auto no-marker, explicit visible, manual/automatic hidden,
HA-disabled, orphaned, unverified, removed active, removed missing,
device-parent tombstone + live entity child, synthetic light group и candidate
без bound room. Каждая включённая exact binding находится ровно в одной базовой
категории.
### AC3 — принятое правило auto/new (unit + smoke)
Автообнаруженное уже видимое устройство без marker находится в «На плане» и
получает «Новое», если его runtime id есть в `new_device_ids`. Открытие каталога
и Find badge не снимают; Edit снимает по текущему контракту.
### AC4 — lifecycle и HA status независимы (unit)
`hidden:true + ha_disabled` остаётся в «Скрытые» с двумя объяснениями;
`hidden:false + ha_disabled` остаётся в «На плане», но Show/Find недоступны.
Limited registry не создаёт ложный disabled/orphaned/readd.
### AC5 — exact binding и повторное добавление (unit + smoke)
`device:D` tombstone и живой `entity:X` образуют две корректные строки.
Re-add exact binding заменяет только её tombstone, не создаёт дубликат, не
возвращает старую позицию/файлы/trail и сохраняет соседний parent/child lifecycle.
### AC6 — действия используют существующие транзакции (smoke)
Find, Edit, Hide, Show, Add, Hide from list и Re-add дают результат §10.3.
Cancel из Add/Re-add не меняет данные; Delete остаётся подтверждаемым действием
существующего dialog.
### AC7 — read-only открытие (unit + smoke)
Snapshot config, layout и обе revisions до/после открытия, поиска, tab switch,
reason expand, Show more и Find идентичны. Ни один из этих путей не вызывает
config/layout websocket write.
### AC8 — возврат и live refresh (smoke)
После Edit/Save/Cancel каталог возвращается с прежними tab/search/filter и
логическим scroll anchor. Registry/config refresh пересчитывает строку без
закрытия открытого device dialog и без дубликатов.
### AC9 — поиск и большие реестры (unit + smoke)
Поиск находит строку по каждому полю §10.2, включая entity за пределом первых
200 исходных записей. Progressive rendering не меняет total counts и не
приводит к вложенному полному обходу registry на каждую строку.
### AC10 — accessibility/responsive (smoke + golden + review)
Диалог работает с клавиатуры, восстанавливает focus, не имеет горизонтального
скролла на narrow fixture и корректен в RU/EN и light/dark. Touch View и kiosk
не получают новых интерактивных слоёв.
### AC11 — compatibility (unit + existing regressions)
Текущие тесты `devices`, `ha-binding-status`, `device-presentation`,
`binding_picker` и `hidden_flag` сохраняют смысл. Старые markers/settings читаются
без миграции, а каталог не меняет auto-placement, filtering, light aggregation,
LQI/climate или opening references.
## 16. План автотестов
- `test/device-inbox.test.mjs` — pure resolver, reason/capability/search/sort,
large registry и exact-binding matrix;
- `test/devices.test.mjs`, `test/ha-binding-status.test.mjs` — соседние
регрессии lifecycle;
- `demo/smoke_device_inbox.mjs` — реальный dialog, read-only assertions,
row actions, return state, late registry refresh;
- адаптация `demo/smoke_binding_picker.mjs` и `demo/smoke_hidden_flag.mjs` без
потери существующих проверок;
- golden: desktop RU/EN, light/dark; narrow RU с длинными причинами;
- `node scripts/smoke-select.mjs --base origin/dev --head HEAD` определяет
дополнительный набор перед код-ревью.
Минимальные локальные гейты реализации:
```text
npx tsc --noEmit
npm test
npm run bundle:sync
node scripts/check-docs.mjs
node scripts/smoke-select.mjs --base origin/dev --head HEAD
node demo/smoke_device_inbox.mjs
node demo/smoke_binding_picker.mjs
node demo/smoke_hidden_flag.mjs
npm run golden:verify
```
## 17. Производительность
- полный индекс может включать тысячи entity, но DOM получает не более 100
строк за одну порцию;
- поиск и counts считаются по полному memoized snapshot;
- registry refresh инвалидирует snapshot один раз на revision;
- новый код не добавляет рендеров или observers в View/киоск;
- performance benchmark обязателен только если `smoke-select` либо ревью
выявят затронутый бюджет; synthetic unit на большой registry остаётся
обязательным.
## 18. Риски
| Риск | Защита |
|---|---|
| Auto no-marker ошибочно показан как не добавленный | Effective runtime presence важнее наличия marker; AC3 |
| Device/entity parent-child дедуплицируются слишком широко | Exact binding key и fixture #262; AC5 |
| Limited registry создаёт ложные причины | Только positive evidence; AC4 |
| Catalog и Add picker расходятся | Один общий eligibility helper |
| Открытие UI материализует данные | Явный no-write contract и websocket assertions; AC7 |
| Историческая причина скрытия выдумывается | Generic `automatic_hidden` fallback |
| Большой registry блокирует dialog | O(N) snapshot + progressive rendering |
| Возврат из nested flow теряет контекст | Локальный `returnToInbox` snapshot; AC8 |
| #29 поглощает redesign фильтров | #44 явно вне скоупа |
## 19. Откат
Изменение не мигрирует config и не создаёт новых persisted-полей. Откат
возвращает две прежние кнопки и Add picker; markers, tombstones, hidden flags,
layout и attachments остаются совместимыми. Созданный через «Скрыть из списка»
`hidden:true` marker уже поддерживается старым UI и может быть показан обычным
способом.
## 20. Release-артефакты
- `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` со ссылкой на #29;
- раздел Device editor в `docs/USER-GUIDE.md` и
`docs/USER-GUIDE.ru.md`;
- `docs/FILTERING.md` — каталог как единая presentation surface, без изменения
lifecycle semantics;
- `docs/ARCHITECTURE.md` — pure resolver и shared eligibility helper;
- актуальные golden baselines и публичный screenshot редактора устройств, если
документационный capture затронут;
- backend, security и model migration release notes: **не требуются**.
## 21. Принятые технические предположения — ревьюер может менять свободно
1. Pure resolver живёт в `src/device-inbox.ts`, а не внутри Lit-компонента.
2. Existing Add eligibility извлекается в общий helper; имя модуля не является
продуктовым контрактом.
3. Tab/search/filter/scroll state хранится только в экземпляре карточки.
4. Progressive page = 100 строк; конкретное число можно скорректировать по
golden/performance без изменения пользовательской семантики.
5. Точная историческая причина seed не сохраняется; при отсутствии доказательства
используется `automatic_hidden`.
6. #44 расширит тот же dialog advanced-настройками позже, но #29 не ждёт #44 и
не меняет discovery settings.