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

23 KiB
Raw Blame History

ТЗ #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, чтобы не создавать скрытую миграцию старых конфигураций.