23 KiB
ТЗ #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 список только роботов, сохраняя
межэтажные маршруты и визуальное поведение.
Подтверждённая проблема
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.- Single-space export в
custom_components/houseplan/import_export.pyзаписываетkept_routes or None. Когда все маршруты отфильтрованы, явная пустота теряется, а оставшийся в marker legacycalibrationможет ожить при последующем чтении. - Заявленная в issue случайная перестановка missing-space строк по коду не
подтверждается: comparator уже детерминирован по space order,
map_idи source, а schema запрещает повтор пары source/map. Подтверждён UX-недочёт: строки лишь неявно идут в конце и не имеют отдельной группы. _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 показывает маршруты в следующем порядке:
- маршруты существующих пространств — по текущему порядку spaces, затем по
map_id, source и route id как детерминированному tie-breaker; - отдельная группа потерянных маршрутов после всех валидных групп — по
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продолжают читать legacycalibration. - Уже сохранённый
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-emptymap_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, чтобы не создавать скрытую миграцию старых конфигураций.