Files
houseplan-card/docs/specs/443-vacuum-route-polish.md
T
2026-09-03 23:03:11 +03:00

323 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.
# ТЗ #443 — Полиш маршрутов карт робота
- Issue: https://github.com/Matysh/houseplan-card/issues/443
- Приоритет: P3, `bug` / `polish` / `vacuum`
- Статус ТЗ: готово к ревью
- Маршрут: full; меняются frontend и backend semantics, single-space export,
локализованный Device editor и performance-sensitive render snapshot
- Связанные контракты: #162 (маршрутизация карт по пространствам), #441
(атомарное добавление новой карты робота)
## Сценарий
Home admin удаляет у робота последний явный маршрут карты, редактирует старую
конфигурацию с маршрутами в удалённые пространства либо показывает большой
многоэтажный план с сотнями устройств. Пустой список маршрутов должен означать
осознанное отсутствие маршрутов, строки с потерянным пространством должны быть
понятно собраны, а View не должен повторно просматривать все не относящиеся к
роботам устройства ради vacuum overlay.
## Что человек увидит до и после
**До:** `map_routes: []` может снова включить оставшийся legacy `calibration` и
показать робота/карту там, откуда пользователь удалил маршрут. При экспорте
одного пространства отфильтрованный до пустоты список может превратиться в
`null` и создать тот же эффект после импорта. Маршруты в удалённые пространства
помечены построчно и оказываются в конце, но визуально не образуют явную группу.
Каждый render vacuum layer ещё раз перебирает все устройства плана.
**После:** любой массив `map_routes`, включая пустой, является явной authority;
legacy fallback действует только при отсутствующем или `null` поле. Экспорт не
теряет осознанную пустоту. Потерянные маршруты собраны в локализованную группу
«Пространство удалено», остаются редактируемыми и имеют стабильный порядок.
Vacuum renderer получает сохранённый в snapshot список только роботов, сохраняя
межэтажные маршруты и визуальное поведение.
## Подтверждённая проблема
1. `src/vacuum-routes.ts::effectiveRoutes()` и
`custom_components/houseplan/vacuum_routes.py::effective_routes()` выбирают
explicit routes только при непустом массиве/списке. Это противоречит #162
§7.3 и `docs/VACUUM.md`: наличие `map_routes` как массива уже означает
единственную route authority.
2. Single-space export в `custom_components/houseplan/import_export.py`
записывает `kept_routes or None`. Когда все маршруты отфильтрованы, явная
пустота теряется, а оставшийся в marker legacy `calibration` может ожить при
последующем чтении.
3. Заявленная в issue случайная перестановка missing-space строк по коду не
подтверждается: comparator уже детерминирован по space order, `map_id` и
source, а schema запрещает повтор пары source/map. Подтверждён UX-недочёт:
строки лишь неявно идут в конце и не имеют отдельной группы.
4. `_captureRenderDeviceSnapshot()` уже обходит все устройства и рассчитывает
vacuum facts. Затем `_renderVacuums(this._renderDevices, ...)` повторно
перебирает весь device roster, хотя vacuum renderer нужны только роботы.
## Скоуп
В скоупе:
- единая frontend/backend семантика absent, `null`, пустого и непустого
`map_routes`;
- сохранение явного пустого массива при single-space export;
- явная локализованная группа строк, чьё `space_id` больше не существует;
- стабильный порядок валидных и потерянных маршрутов;
- vacuum-only subset в immutable render snapshot и его использование слоем
vacuum overlay;
- сохранение межэтажного отображения робота, hidden/static правил и continuity
при временном отсутствии нового snapshot;
- unit, backend, browser smoke, mutation и performance evidence;
- актуализация документации и обоих changelog.
## Не-скоуп
- новый каталог vacuum integrations, обнаружение карт или изменение протокола
источников;
- изменение route identity, калибровки, puck/trail/room outline либо команд
уборки;
- автоматический выбор нового пространства для потерянного маршрута;
- автоматическое удаление missing-space route;
- миграция или eager rewrite сохранённых legacy markers;
- изменение семантики malformed non-array `map_routes`;
- оптимизация остальных проходов по device roster и полный рефактор render
snapshot;
- изменение удаления пространства за пределами экспортного edge case;
- изменение static space-card UX или редакторов вне vacuum maps section.
## Контракт поведения
### 1. Authority `map_routes`
Frontend и backend используют одну таблицу решений:
| Состояние marker | Эффективные маршруты |
|---|---|
| поле `map_routes` отсутствует | legacy fallback из `calibration`, как сейчас |
| `map_routes: null` | legacy fallback из `calibration`, как сейчас |
| `map_routes: []` | пустой explicit result; legacy полностью игнорируется |
| `map_routes: [ ... ]` | только нормализованные explicit routes; legacy игнорируется |
Проверка должна зависеть от типа массива/списка, а не от его truthiness или
длины. Пустой массив не создаёт overlay, trail, puck или route-space
presentation из legacy calibration.
Malformed non-array значения остаются на прежнем compatibility path; #443 не
расширяет schema и не пытается молча исправлять неизвестные будущие формы.
Никакой фоновой записи конфигурации при чтении не выполняется.
### 2. Single-space export
Если исходный vacuum marker содержит массив `map_routes`, export одного
пространства сохраняет `map_routes` массивом даже тогда, когда фильтрация не
оставила ни одного route. Результат в этом случае — `[]`, а не `null`,
отсутствующее поле или возврат legacy calibration.
Если исходное поле отсутствовало или было `null`, действующее legacy-compatible
поведение не меняется. Экспорт не создаёт synthetic routes и не удаляет
legacy-поля из source marker: безопасность обеспечивается сохранённой explicit
empty authority.
### 3. Группировка потерянных пространств
Device editor показывает маршруты в следующем порядке:
1. маршруты существующих пространств — по текущему порядку spaces, затем по
`map_id`, source и route id как детерминированному tie-breaker;
2. отдельная группа потерянных маршрутов после всех валидных групп — по
`map_id`, source и route id.
Группа имеет видимый локализованный заголовок с семантикой «Пространство
удалено». Рекомендуемые строки:
- ru: «Пространство удалено»;
- en: “Deleted space”;
- de: “Gelöschter Bereich”;
- fr: “Espace supprimé”.
Существующая построчная индикация missing-space и общий warning сохраняются:
заголовок помогает сканировать список, но не является единственным объяснением
ошибки. Каждая строка по-прежнему доступна для выбора, редактирования и
удаления; House Plan не угадывает замену пространства. Pending draft добавления
маршрута не смешивается с группой и сохраняет контракт #441.
Если потерянных маршрутов нет, заголовок и дополнительная группа не
рендерятся. Порядок валидных строк визуально не меняется.
### 4. Vacuum-only render snapshot
Capture одного device snapshot формирует immutable vacuum-only subset из той
же согласованной копии device roster, на которой рассчитаны facts. Vacuum
renderer в обычном View перебирает этот subset, а не полный список устройств.
Subset обязан содержать всех роботов плана, а не только устройства текущего
пространства. Поэтому сохраняются:
- показ робота на route-space другого этажа относительно пространства базы;
- выбор map route и route facts из того же snapshot;
- suppression скрытых и static devices;
- отсутствие повторного resolve route authority внутри renderer;
- snapshot continuity: незавершённый новый capture не смешивает старые devices
с новыми facts.
Fallback полного списка допустим только на существующем snapshot-less
инициализационном/тестовом пути и не должен становиться обычным render path.
Изменение сокращает повторный render scan с `O(N)` до `O(V)`, где `N` — все
device markers, а `V` — vacuum markers; capture остаётся `O(N)` и не обязан
ускоряться в этой задаче.
## Данные и совместимость
- Schema version и persisted shape не меняются; миграции нет.
- Старые конфигурации без `map_routes` и с `map_routes: null` продолжают читать
legacy `calibration`.
- Уже сохранённый `map_routes: []` меняет только ошибочную read semantics и
перестаёт оживлять legacy routes.
- Frontend и backend обязаны дать одинаковый результат на четырёх состояниях
таблицы authority.
- Atomic add/dedup/rollback из #441 не меняются.
## Touch, клавиатура и доступность
Новых жестов и targets нет. Группа missing-space получает обычный
неинтерактивный текстовый заголовок и структурную связь со следующими строками;
состояние не передаётся только цветом. Tab order, действия строк, focus restore
и touch targets остаются прежними. Новая строка проходит en/ru/de/fr completeness
и locale smoke.
## Ошибки, гонки и крайние случаи
| Случай | Ожидаемое поведение |
|---|---|
| `map_routes: []` и непустой legacy `calibration` | 0 effective routes, legacy не читается |
| `map_routes: null` и legacy calibration | прежний legacy route |
| все routes отфильтрованы single-space export | в export остаётся `map_routes: []` |
| один valid и несколько missing routes | valid group первая, затем одна missing-space group со стабильными строками |
| отсутствуют все пространства routes | после draft area показывается только missing-space group; строки редактируемы |
| несколько роботов на разных этажах | subset содержит всех; каждый показывается только по своим route facts |
| roster меняется между HA updates | кадр использует одну immutable snapshot revision без смешения |
| нет captured snapshot | существующий fallback не падает и не фильтрует робота по dock space |
| malformed non-array `map_routes` | прежнее compatibility/fail-closed поведение без новой миграции |
## Acceptance criteria и доказательства
### AC1. Единая route authority
TS unit проверяет absent, `null`, `[]` и non-empty `map_routes` при наличии
legacy calibration. Пустой массив даёт ноль routes; непустой — только explicit;
absent/`null` сохраняют legacy.
### AC2. Backend parity
Python unit прогоняет ту же матрицу и сверяет нормализованные результаты с
frontend contract. Как минимум один отрицательный кейс обязан падать при
возврате старого условия «список непуст».
### AC3. Export не оживляет legacy
Backend import/export test экспортирует одно пространство из marker с explicit
routes, полностью удалёнными фильтром, и оставшимся legacy calibration.
Результат содержит `map_routes: []`; повторное вычисление effective routes после
round-trip даёт пустоту.
### AC4. Понятная и стабильная группа
Browser smoke открывает Device editor с valid и несколькими missing routes,
проверяет порядок групп/строк, единственный локализованный заголовок, warning и
доступность edit/delete. Повторный render и перестановка входного словаря spaces
при том же canonical order не меняют порядок. Pending draft остаётся отдельно.
### AC5. Renderer получает только роботов
Unit/contract test создаёт roster с не-vacuum и vacuum markers и проверяет, что
captured subset immutable, содержит только всех vacuum markers и используется
обычным `_renderVacuums` call site. Renderer не выполняет второй полный scan.
### AC6. Межэтажное и snapshot-поведение сохранено
Существующий multifloor vacuum smoke и cold/live continuity test остаются
зелёными: робот с базой на другом этаже отображается в route-space, hidden и
static suppression прежние, старые devices не смешиваются с новыми facts.
Mutation, возвращающий current-space `devs` в vacuum renderer, обязан уронить
межэтажный witness.
### AC7. Производительность не ухудшена
Large-house профиль 60 rooms / 200 devices на exact SHA проходит действующий
performance smoke и budget. Дополнительный structural witness фиксирует
`V < N` и один vacuum-only render pass; отдельный новый timing budget не
вводится, поскольку изменение сокращает сложность и не добавляет visual work.
### AC8. Совместимость и документация
Typecheck, unit и build зелёные. Перед бетой проходят golden, smoke и
performance по канону процесса; Linux CI является каноном полного HA harness.
`docs/VACUUM.md` явно называет пустой массив authority, оба changelog описывают
пользовательское исправление, en/ru/de/fr dictionaries проходят completeness.
## План тестирования
- расширить TS unit для `effectiveRoutes()` таблицей всех состояний поля;
- расширить backend unit `vacuum_routes` и import/export round-trip;
- добавить/расширить browser smoke vacuum route editor для missing-space group;
- расширить render snapshot unit/contract vacuum-only subset;
- сохранить и прогнать multifloor/cold-view vacuum smokes;
- обновить `scripts/mutation-gate.mjs`: explicit-empty mutant возвращает старую
проверку длины и краснеет; cross-floor mutant подменяет vacuum subset на
current-space devices и краснеет;
- в implementation loop запускать только `typecheck`, `unit`, `build`; golden,
smoke и performance запускать перед бетой согласно runbook.
## Карта реализации
- `src/vacuum-routes.ts` — array presence вместо non-empty selection;
- `custom_components/houseplan/vacuum_routes.py` — backend parity;
- `custom_components/houseplan/import_export.py` — сохранение explicit empty;
- `src/editors/vacuum-maps-section.ts` — partitions/groups и stable tie-breaker;
- `src/i18n/support/{en,ru,de,fr}.json` — заголовок missing-space group;
- `src/render-device-snapshot.ts` и `src/houseplan-card.ts` — immutable
vacuum-only subset и renderer call site;
- `test/`, `tests_backend/`, `demo/`, `scripts/mutation-gate.mjs` — witnesses;
- `docs/VACUUM.md`, `docs/CONFIG-COMPATIBILITY.md`,
`docs/USER-GUIDE.ru.md`, `docs/CHANGELOG.md`, `docs/CHANGELOG.ru.md` —
release artifacts.
## Риски и rollback
- Главный compatibility risk — ошибочно отключить legacy fallback для absent или
`null`; его закрывает одинаковая четырёхстрочная матрица в TS и Python.
- Главный render risk — отфильтровать робота по текущему пространству/этажу;
subset строится глобально, а cross-floor mutant обязан краснеть.
- Главный snapshot risk — создать subset из другого поколения devices; список
формируется и замораживается вместе с основной snapshot revision.
- UI-группа может нарушить focus/order; smoke проверяет интерактивные строки, а
заголовок остаётся неинтерактивным.
- Rollback code path не требует data rollback: schema не меняется. Возврат
implementation не переписывает сохранённые конфигурации.
## Release-артефакты
- Обязательны записи в `docs/CHANGELOG.md` и `docs/CHANGELOG.ru.md` в том же
user-visible коммите.
- Обновляется `docs/VACUUM.md` для явного empty-array/export контракта.
- Обновляется `docs/CONFIG-COMPATIBILITY.md`: раздел vacuum map routes явно
различает absent, `null`, explicit empty и non-empty `map_routes`, включая
сохранение `[]` при single-space export.
- Обновляется `docs/USER-GUIDE.ru.md`: раздел «Карты и этажи» описывает новую
видимую группу маршрутов «Пространство удалено» в Device editor.
- Изменение Device editor требует browser screenshot/smoke evidence в
light/dark темах; новый постоянный golden добавляется только если этот экран
уже входит в canonical golden surface, иначе review artifact прикладывается
к issue без расширения baseline.
- Перед бетой обязательны штатные golden, smoke и full performance artifacts;
новый security artifact не требуется.
## Принятые предположения
- Явный массив `map_routes` является authority независимо от длины — это уже
принятое решение #162, а не новый вопрос.
- Требование issue «явная группа» означает отдельный локализованный заголовок
после всех валидных пространств; действия строк не меняются.
- Исправление производительности ограничено устранением второго полного обхода:
отдельный новый latency threshold без подтверждённой деградации не нужен.
- `map_routes: null` остаётся legacy-compatible, чтобы не создавать скрытую
миграцию старых конфигураций.